autoglm-gui 1.4.0__py3-none-any.whl → 1.4.1__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.
- AutoGLM_GUI/__main__.py +0 -4
- AutoGLM_GUI/adb_plus/qr_pair.py +8 -8
- AutoGLM_GUI/agents/__init__.py +20 -0
- AutoGLM_GUI/agents/factory.py +160 -0
- AutoGLM_GUI/agents/mai_adapter.py +627 -0
- AutoGLM_GUI/agents/protocols.py +23 -0
- AutoGLM_GUI/api/__init__.py +48 -7
- AutoGLM_GUI/api/agents.py +61 -17
- AutoGLM_GUI/api/devices.py +12 -18
- AutoGLM_GUI/api/dual_model.py +15 -9
- AutoGLM_GUI/api/health.py +13 -0
- AutoGLM_GUI/api/layered_agent.py +239 -166
- AutoGLM_GUI/api/mcp.py +11 -10
- AutoGLM_GUI/api/version.py +23 -10
- AutoGLM_GUI/api/workflows.py +2 -1
- AutoGLM_GUI/config_manager.py +55 -1
- AutoGLM_GUI/device_adapter.py +263 -0
- AutoGLM_GUI/device_protocol.py +266 -0
- AutoGLM_GUI/devices/__init__.py +49 -0
- AutoGLM_GUI/devices/adb_device.py +205 -0
- AutoGLM_GUI/devices/mock_device.py +183 -0
- AutoGLM_GUI/devices/remote_device.py +172 -0
- AutoGLM_GUI/dual_model/decision_model.py +4 -4
- AutoGLM_GUI/exceptions.py +3 -3
- AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +2 -2
- AutoGLM_GUI/metrics.py +13 -20
- AutoGLM_GUI/phone_agent_manager.py +219 -134
- AutoGLM_GUI/phone_agent_patches.py +2 -1
- AutoGLM_GUI/platform_utils.py +5 -2
- AutoGLM_GUI/schemas.py +47 -0
- AutoGLM_GUI/scrcpy_stream.py +17 -13
- AutoGLM_GUI/server.py +3 -1
- AutoGLM_GUI/socketio_server.py +16 -4
- AutoGLM_GUI/state.py +10 -30
- AutoGLM_GUI/static/assets/{about-DeclntHg.js → about-_XNhzQZX.js} +1 -1
- AutoGLM_GUI/static/assets/chat-DwJpiAWf.js +126 -0
- AutoGLM_GUI/static/assets/{dialog-BfdcBs1x.js → dialog-B3uW4T8V.js} +3 -3
- AutoGLM_GUI/static/assets/index-Cpv2gSF1.css +1 -0
- AutoGLM_GUI/static/assets/{index-zQ4KKDHt.js → index-Cy8TmmHV.js} +1 -1
- AutoGLM_GUI/static/assets/{index-DHF1NZh0.js → index-UYYauTly.js} +6 -6
- AutoGLM_GUI/static/assets/{workflows-xiplap-r.js → workflows-Du_de-dt.js} +1 -1
- AutoGLM_GUI/static/index.html +2 -2
- AutoGLM_GUI/types.py +125 -0
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.4.1.dist-info}/METADATA +83 -4
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.4.1.dist-info}/RECORD +54 -37
- mai_agent/base.py +137 -0
- mai_agent/mai_grounding_agent.py +263 -0
- mai_agent/mai_naivigation_agent.py +526 -0
- mai_agent/prompt.py +148 -0
- mai_agent/unified_memory.py +67 -0
- mai_agent/utils.py +73 -0
- AutoGLM_GUI/config.py +0 -23
- AutoGLM_GUI/static/assets/chat-Iut2yhSw.js +0 -125
- AutoGLM_GUI/static/assets/index-5hCCwHA7.css +0 -1
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.4.1.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.4.1.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.4.1.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/config_manager.py
CHANGED
|
@@ -58,6 +58,23 @@ class ConfigModel(BaseModel):
|
|
|
58
58
|
decision_model_name: str = ""
|
|
59
59
|
decision_api_key: str = ""
|
|
60
60
|
|
|
61
|
+
# Agent 类型配置
|
|
62
|
+
agent_type: str = "glm" # Agent type (e.g., "glm", "mai")
|
|
63
|
+
agent_config_params: dict | None = None # Agent-specific configuration
|
|
64
|
+
|
|
65
|
+
# Agent 执行配置
|
|
66
|
+
default_max_steps: int = 100 # 单次任务最大执行步数
|
|
67
|
+
|
|
68
|
+
@field_validator("default_max_steps")
|
|
69
|
+
@classmethod
|
|
70
|
+
def validate_default_max_steps(cls, v: int) -> int:
|
|
71
|
+
"""验证 default_max_steps 范围."""
|
|
72
|
+
if v <= 0:
|
|
73
|
+
raise ValueError("default_max_steps must be positive")
|
|
74
|
+
if v > 1000:
|
|
75
|
+
raise ValueError("default_max_steps must be <= 1000")
|
|
76
|
+
return v
|
|
77
|
+
|
|
61
78
|
@field_validator("base_url")
|
|
62
79
|
@classmethod
|
|
63
80
|
def validate_base_url(cls, v: str) -> str:
|
|
@@ -98,6 +115,11 @@ class ConfigLayer:
|
|
|
98
115
|
decision_base_url: Optional[str] = None
|
|
99
116
|
decision_model_name: Optional[str] = None
|
|
100
117
|
decision_api_key: Optional[str] = None
|
|
118
|
+
# Agent 类型配置
|
|
119
|
+
agent_type: Optional[str] = None
|
|
120
|
+
agent_config_params: Optional[dict] = None
|
|
121
|
+
# Agent 执行配置
|
|
122
|
+
default_max_steps: Optional[int] = None
|
|
101
123
|
|
|
102
124
|
source: ConfigSource = ConfigSource.DEFAULT
|
|
103
125
|
|
|
@@ -129,6 +151,9 @@ class ConfigLayer:
|
|
|
129
151
|
"decision_base_url": self.decision_base_url,
|
|
130
152
|
"decision_model_name": self.decision_model_name,
|
|
131
153
|
"decision_api_key": self.decision_api_key,
|
|
154
|
+
"agent_type": self.agent_type,
|
|
155
|
+
"agent_config_params": self.agent_config_params,
|
|
156
|
+
"default_max_steps": self.default_max_steps,
|
|
132
157
|
}.items()
|
|
133
158
|
if v is not None
|
|
134
159
|
}
|
|
@@ -188,6 +213,9 @@ class UnifiedConfigManager:
|
|
|
188
213
|
base_url="",
|
|
189
214
|
model_name="autoglm-phone-9b",
|
|
190
215
|
api_key="EMPTY",
|
|
216
|
+
agent_type="glm",
|
|
217
|
+
agent_config_params=None,
|
|
218
|
+
default_max_steps=100,
|
|
191
219
|
source=ConfigSource.DEFAULT,
|
|
192
220
|
)
|
|
193
221
|
|
|
@@ -300,6 +328,11 @@ class UnifiedConfigManager:
|
|
|
300
328
|
decision_base_url=config_data.get("decision_base_url"),
|
|
301
329
|
decision_model_name=config_data.get("decision_model_name"),
|
|
302
330
|
decision_api_key=config_data.get("decision_api_key"),
|
|
331
|
+
agent_type=config_data.get(
|
|
332
|
+
"agent_type", "glm"
|
|
333
|
+
), # 默认 'glm',兼容旧配置
|
|
334
|
+
agent_config_params=config_data.get("agent_config_params"),
|
|
335
|
+
default_max_steps=config_data.get("default_max_steps"),
|
|
303
336
|
source=ConfigSource.FILE,
|
|
304
337
|
)
|
|
305
338
|
self._effective_config = None # 清除缓存
|
|
@@ -331,6 +364,9 @@ class UnifiedConfigManager:
|
|
|
331
364
|
decision_base_url: Optional[str] = None,
|
|
332
365
|
decision_model_name: Optional[str] = None,
|
|
333
366
|
decision_api_key: Optional[str] = None,
|
|
367
|
+
agent_type: Optional[str] = None,
|
|
368
|
+
agent_config_params: Optional[dict] = None,
|
|
369
|
+
default_max_steps: Optional[int] = None,
|
|
334
370
|
merge_mode: bool = True,
|
|
335
371
|
) -> bool:
|
|
336
372
|
"""
|
|
@@ -344,6 +380,9 @@ class UnifiedConfigManager:
|
|
|
344
380
|
decision_base_url: 决策模型 Base URL
|
|
345
381
|
decision_model_name: 决策模型名称
|
|
346
382
|
decision_api_key: 决策模型 API key
|
|
383
|
+
agent_type: Agent 类型(可选,如 "glm", "mai")
|
|
384
|
+
agent_config_params: Agent 特定配置参数(可选)
|
|
385
|
+
default_max_steps: 默认最大执行步数(可选)
|
|
347
386
|
merge_mode: 是否合并现有配置(True: 保留未提供的字段)
|
|
348
387
|
|
|
349
388
|
Returns:
|
|
@@ -354,7 +393,7 @@ class UnifiedConfigManager:
|
|
|
354
393
|
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
355
394
|
|
|
356
395
|
# 准备新配置
|
|
357
|
-
new_config = {
|
|
396
|
+
new_config: dict[str, str | bool | int | dict | None] = {
|
|
358
397
|
"base_url": base_url,
|
|
359
398
|
"model_name": model_name,
|
|
360
399
|
}
|
|
@@ -369,6 +408,12 @@ class UnifiedConfigManager:
|
|
|
369
408
|
new_config["decision_model_name"] = decision_model_name
|
|
370
409
|
if decision_api_key:
|
|
371
410
|
new_config["decision_api_key"] = decision_api_key
|
|
411
|
+
if agent_type is not None:
|
|
412
|
+
new_config["agent_type"] = agent_type
|
|
413
|
+
if agent_config_params is not None:
|
|
414
|
+
new_config["agent_config_params"] = agent_config_params
|
|
415
|
+
if default_max_steps is not None:
|
|
416
|
+
new_config["default_max_steps"] = default_max_steps
|
|
372
417
|
|
|
373
418
|
# 合并模式:保留现有文件中未提供的字段
|
|
374
419
|
if merge_mode and self._config_path.exists():
|
|
@@ -383,6 +428,9 @@ class UnifiedConfigManager:
|
|
|
383
428
|
"decision_base_url",
|
|
384
429
|
"decision_model_name",
|
|
385
430
|
"decision_api_key",
|
|
431
|
+
"agent_type",
|
|
432
|
+
"agent_config_params",
|
|
433
|
+
"default_max_steps",
|
|
386
434
|
]
|
|
387
435
|
for key in preserve_keys:
|
|
388
436
|
if key not in new_config and key in existing:
|
|
@@ -471,6 +519,9 @@ class UnifiedConfigManager:
|
|
|
471
519
|
"decision_base_url",
|
|
472
520
|
"decision_model_name",
|
|
473
521
|
"decision_api_key",
|
|
522
|
+
"agent_type",
|
|
523
|
+
"agent_config_params",
|
|
524
|
+
"default_max_steps",
|
|
474
525
|
]
|
|
475
526
|
|
|
476
527
|
for key in config_keys:
|
|
@@ -637,6 +688,9 @@ class UnifiedConfigManager:
|
|
|
637
688
|
"decision_base_url": config.decision_base_url,
|
|
638
689
|
"decision_model_name": config.decision_model_name,
|
|
639
690
|
"decision_api_key": config.decision_api_key,
|
|
691
|
+
"agent_type": config.agent_type,
|
|
692
|
+
"agent_config_params": config.agent_config_params,
|
|
693
|
+
"default_max_steps": config.default_max_steps,
|
|
640
694
|
}
|
|
641
695
|
|
|
642
696
|
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Device Protocol Adapter for phone_agent integration.
|
|
2
|
+
|
|
3
|
+
This module provides an adapter that bridges DeviceProtocol implementations
|
|
4
|
+
to the interface expected by phone_agent's DeviceFactory.
|
|
5
|
+
|
|
6
|
+
The adapter allows injecting any DeviceProtocol implementation (ADB, Mock, Remote)
|
|
7
|
+
into phone_agent without modifying the third-party code.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from AutoGLM_GUI.device_adapter import inject_device_protocol
|
|
11
|
+
>>> from AutoGLM_GUI.devices import MockDevice, ADBDevice
|
|
12
|
+
>>>
|
|
13
|
+
>>> # For testing: inject mock device
|
|
14
|
+
>>> mock = MockDevice("mock_001", state_machine)
|
|
15
|
+
>>> inject_device_protocol(lambda _: mock)
|
|
16
|
+
>>>
|
|
17
|
+
>>> # For production: inject ADB device
|
|
18
|
+
>>> devices = {"phone_1": ADBDevice("emulator-5554")}
|
|
19
|
+
>>> inject_device_protocol(lambda device_id: devices[device_id])
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from typing import Callable
|
|
23
|
+
|
|
24
|
+
import phone_agent.device_factory as device_factory_module
|
|
25
|
+
from AutoGLM_GUI.device_protocol import DeviceProtocol, Screenshot
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DeviceProtocolAdapter:
|
|
29
|
+
"""
|
|
30
|
+
Adapter that bridges DeviceProtocol to phone_agent's DeviceFactory interface.
|
|
31
|
+
|
|
32
|
+
This adapter wraps a DeviceProtocol getter function and exposes the same
|
|
33
|
+
interface as phone_agent's DeviceFactory, allowing seamless injection.
|
|
34
|
+
|
|
35
|
+
The adapter handles:
|
|
36
|
+
- Routing device operations to the correct DeviceProtocol instance
|
|
37
|
+
- Converting between DeviceProtocol and DeviceFactory method signatures
|
|
38
|
+
- Managing device_id parameters (phone_agent passes device_id to each method)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
get_device: Callable[[str | None], DeviceProtocol],
|
|
44
|
+
default_device_id: str | None = None,
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
Initialize the adapter.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
get_device: Function that returns a DeviceProtocol given a device_id.
|
|
51
|
+
If device_id is None, should return a default device.
|
|
52
|
+
default_device_id: Default device ID to use when None is passed.
|
|
53
|
+
"""
|
|
54
|
+
self._get_device = get_device
|
|
55
|
+
self._default_device_id = default_device_id
|
|
56
|
+
# For compatibility with code that checks device_type
|
|
57
|
+
self.device_type = "protocol_adapter"
|
|
58
|
+
|
|
59
|
+
def _device(self, device_id: str | None) -> DeviceProtocol:
|
|
60
|
+
"""Get device for the given ID."""
|
|
61
|
+
effective_id = device_id or self._default_device_id
|
|
62
|
+
return self._get_device(effective_id)
|
|
63
|
+
|
|
64
|
+
# === Screenshot ===
|
|
65
|
+
def get_screenshot(
|
|
66
|
+
self, device_id: str | None = None, timeout: int = 10
|
|
67
|
+
) -> Screenshot:
|
|
68
|
+
"""Get screenshot from device."""
|
|
69
|
+
return self._device(device_id).get_screenshot(timeout)
|
|
70
|
+
|
|
71
|
+
# === Input Operations ===
|
|
72
|
+
def tap(
|
|
73
|
+
self, x: int, y: int, device_id: str | None = None, delay: float | None = None
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Tap at coordinates."""
|
|
76
|
+
self._device(device_id).tap(x, y, delay)
|
|
77
|
+
|
|
78
|
+
def double_tap(
|
|
79
|
+
self, x: int, y: int, device_id: str | None = None, delay: float | None = None
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Double tap at coordinates."""
|
|
82
|
+
self._device(device_id).double_tap(x, y, delay)
|
|
83
|
+
|
|
84
|
+
def long_press(
|
|
85
|
+
self,
|
|
86
|
+
x: int,
|
|
87
|
+
y: int,
|
|
88
|
+
duration_ms: int = 3000,
|
|
89
|
+
device_id: str | None = None,
|
|
90
|
+
delay: float | None = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Long press at coordinates."""
|
|
93
|
+
self._device(device_id).long_press(x, y, duration_ms, delay)
|
|
94
|
+
|
|
95
|
+
def swipe(
|
|
96
|
+
self,
|
|
97
|
+
start_x: int,
|
|
98
|
+
start_y: int,
|
|
99
|
+
end_x: int,
|
|
100
|
+
end_y: int,
|
|
101
|
+
duration_ms: int | None = None,
|
|
102
|
+
device_id: str | None = None,
|
|
103
|
+
delay: float | None = None,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Swipe from start to end."""
|
|
106
|
+
self._device(device_id).swipe(
|
|
107
|
+
start_x, start_y, end_x, end_y, duration_ms, delay
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def type_text(self, text: str, device_id: str | None = None) -> None:
|
|
111
|
+
"""Type text."""
|
|
112
|
+
self._device(device_id).type_text(text)
|
|
113
|
+
|
|
114
|
+
def clear_text(self, device_id: str | None = None) -> None:
|
|
115
|
+
"""Clear text."""
|
|
116
|
+
self._device(device_id).clear_text()
|
|
117
|
+
|
|
118
|
+
# === Navigation ===
|
|
119
|
+
def back(self, device_id: str | None = None, delay: float | None = None) -> None:
|
|
120
|
+
"""Press back button."""
|
|
121
|
+
self._device(device_id).back(delay)
|
|
122
|
+
|
|
123
|
+
def home(self, device_id: str | None = None, delay: float | None = None) -> None:
|
|
124
|
+
"""Press home button."""
|
|
125
|
+
self._device(device_id).home(delay)
|
|
126
|
+
|
|
127
|
+
def launch_app(
|
|
128
|
+
self, app_name: str, device_id: str | None = None, delay: float | None = None
|
|
129
|
+
) -> bool:
|
|
130
|
+
"""Launch an app."""
|
|
131
|
+
return self._device(device_id).launch_app(app_name, delay)
|
|
132
|
+
|
|
133
|
+
# === State Query ===
|
|
134
|
+
def get_current_app(self, device_id: str | None = None) -> str:
|
|
135
|
+
"""Get current app name."""
|
|
136
|
+
return self._device(device_id).get_current_app()
|
|
137
|
+
|
|
138
|
+
# === Keyboard Management ===
|
|
139
|
+
def detect_and_set_adb_keyboard(self, device_id: str | None = None) -> str:
|
|
140
|
+
"""Detect and set keyboard."""
|
|
141
|
+
return self._device(device_id).detect_and_set_adb_keyboard()
|
|
142
|
+
|
|
143
|
+
def restore_keyboard(self, ime: str, device_id: str | None = None) -> None:
|
|
144
|
+
"""Restore keyboard."""
|
|
145
|
+
self._device(device_id).restore_keyboard(ime)
|
|
146
|
+
|
|
147
|
+
# === Device Management ===
|
|
148
|
+
def list_devices(self) -> list[str]:
|
|
149
|
+
"""
|
|
150
|
+
List connected devices.
|
|
151
|
+
|
|
152
|
+
Note: This is a simplified implementation. For full device listing,
|
|
153
|
+
use ADBDeviceManager.list_devices() directly.
|
|
154
|
+
"""
|
|
155
|
+
# This is called by some parts of phone_agent
|
|
156
|
+
# Return the default device if available
|
|
157
|
+
if self._default_device_id:
|
|
158
|
+
return [self._default_device_id]
|
|
159
|
+
return []
|
|
160
|
+
|
|
161
|
+
def get_connection_class(self):
|
|
162
|
+
"""Not applicable for protocol adapter."""
|
|
163
|
+
raise NotImplementedError(
|
|
164
|
+
"Protocol adapter does not support get_connection_class. "
|
|
165
|
+
"Use ADBDeviceManager for connection management."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# Store original factory for restoration
|
|
170
|
+
_original_factory = None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def inject_device_protocol(
|
|
174
|
+
get_device: Callable[[str | None], DeviceProtocol],
|
|
175
|
+
default_device_id: str | None = None,
|
|
176
|
+
) -> DeviceProtocolAdapter:
|
|
177
|
+
"""
|
|
178
|
+
Inject a DeviceProtocol implementation into phone_agent.
|
|
179
|
+
|
|
180
|
+
This replaces phone_agent's global _device_factory with an adapter
|
|
181
|
+
that routes all device operations through the provided DeviceProtocol.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
get_device: Function that returns a DeviceProtocol given a device_id.
|
|
185
|
+
default_device_id: Default device ID when None is passed.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
The adapter instance (for inspection or further configuration).
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
>>> # Single mock device
|
|
192
|
+
>>> mock = MockDevice("mock_001", state_machine)
|
|
193
|
+
>>> inject_device_protocol(lambda _: mock)
|
|
194
|
+
>>>
|
|
195
|
+
>>> # Multiple devices
|
|
196
|
+
>>> devices = {
|
|
197
|
+
... "phone_1": ADBDevice("emulator-5554"),
|
|
198
|
+
... "phone_2": RemoteDevice("phone_2", "http://remote:8080"),
|
|
199
|
+
... }
|
|
200
|
+
>>> inject_device_protocol(lambda did: devices.get(did, devices["phone_1"]))
|
|
201
|
+
"""
|
|
202
|
+
# TODO: 不应该依赖这种全部变量
|
|
203
|
+
global _original_factory
|
|
204
|
+
|
|
205
|
+
# Save original factory if not already saved
|
|
206
|
+
if _original_factory is None:
|
|
207
|
+
_original_factory = device_factory_module._device_factory
|
|
208
|
+
|
|
209
|
+
# Create and inject adapter
|
|
210
|
+
adapter = DeviceProtocolAdapter(get_device, default_device_id)
|
|
211
|
+
device_factory_module._device_factory = adapter
|
|
212
|
+
|
|
213
|
+
return adapter
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def restore_device_factory() -> None:
|
|
217
|
+
"""
|
|
218
|
+
Restore the original device factory.
|
|
219
|
+
|
|
220
|
+
Call this after testing to restore normal operation.
|
|
221
|
+
"""
|
|
222
|
+
global _original_factory
|
|
223
|
+
|
|
224
|
+
if _original_factory is not None:
|
|
225
|
+
device_factory_module._device_factory = _original_factory
|
|
226
|
+
_original_factory = None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class DeviceProtocolContext:
|
|
230
|
+
"""
|
|
231
|
+
Context manager for temporarily injecting a DeviceProtocol.
|
|
232
|
+
|
|
233
|
+
Example:
|
|
234
|
+
>>> with DeviceProtocolContext(lambda _: mock_device):
|
|
235
|
+
... agent.run("test instruction")
|
|
236
|
+
>>> # Original factory is automatically restored
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
def __init__(
|
|
240
|
+
self,
|
|
241
|
+
get_device: Callable[[str | None], DeviceProtocol],
|
|
242
|
+
default_device_id: str | None = None,
|
|
243
|
+
):
|
|
244
|
+
"""
|
|
245
|
+
Initialize context.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
get_device: Function that returns a DeviceProtocol given a device_id.
|
|
249
|
+
default_device_id: Default device ID when None is passed.
|
|
250
|
+
"""
|
|
251
|
+
self._get_device = get_device
|
|
252
|
+
self._default_device_id = default_device_id
|
|
253
|
+
self._original_factory = None
|
|
254
|
+
|
|
255
|
+
def __enter__(self) -> DeviceProtocolAdapter:
|
|
256
|
+
"""Enter context and inject adapter."""
|
|
257
|
+
self._original_factory = device_factory_module._device_factory
|
|
258
|
+
return inject_device_protocol(self._get_device, self._default_device_id)
|
|
259
|
+
|
|
260
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
261
|
+
"""Exit context and restore original factory."""
|
|
262
|
+
device_factory_module._device_factory = self._original_factory
|
|
263
|
+
return None
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Device Protocol - Abstract interface for device operations.
|
|
2
|
+
|
|
3
|
+
This module defines the protocol (interface) that all device implementations
|
|
4
|
+
must follow. The actual implementation can be:
|
|
5
|
+
- ADB (local subprocess calls)
|
|
6
|
+
- Accessibility Service
|
|
7
|
+
- Remote HTTP/gRPC calls
|
|
8
|
+
- Mock (for testing)
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
>>> from AutoGLM_GUI.devices import ADBDevice, MockDevice
|
|
12
|
+
>>>
|
|
13
|
+
>>> # Production: use ADB
|
|
14
|
+
>>> device = ADBDevice("emulator-5554")
|
|
15
|
+
>>> screenshot = device.get_screenshot()
|
|
16
|
+
>>> device.tap(100, 200)
|
|
17
|
+
>>>
|
|
18
|
+
>>> # Testing: use Mock with state machine
|
|
19
|
+
>>> mock = MockDevice("mock_001", state_machine)
|
|
20
|
+
>>> screenshot = mock.get_screenshot() # Returns state machine's screenshot
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import Protocol, runtime_checkable
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Screenshot:
|
|
29
|
+
"""Screenshot result from device."""
|
|
30
|
+
|
|
31
|
+
base64_data: str
|
|
32
|
+
width: int
|
|
33
|
+
height: int
|
|
34
|
+
is_sensitive: bool = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class DeviceInfo:
|
|
39
|
+
"""Information about a connected device."""
|
|
40
|
+
|
|
41
|
+
device_id: str
|
|
42
|
+
status: str # "online" | "offline" | "unauthorized"
|
|
43
|
+
model: str | None = None
|
|
44
|
+
platform: str = "android" # "android" | "ios" | "harmonyos"
|
|
45
|
+
connection_type: str = "usb" # "usb" | "wifi" | "remote"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@runtime_checkable
|
|
49
|
+
class DeviceProtocol(Protocol):
|
|
50
|
+
"""
|
|
51
|
+
Device operation protocol - all device implementations must follow this interface.
|
|
52
|
+
|
|
53
|
+
This protocol abstracts device operations, allowing the control logic to be
|
|
54
|
+
independent of the actual device implementation (ADB, Accessibility, Remote, etc.).
|
|
55
|
+
|
|
56
|
+
The concrete implementation decides HOW to perform operations:
|
|
57
|
+
- ADBDevice: Uses `adb shell input tap` commands
|
|
58
|
+
- AccessibilityDevice: Uses Android Accessibility Service
|
|
59
|
+
- RemoteDevice: Sends HTTP/gRPC requests to a remote agent
|
|
60
|
+
- MockDevice: Routes operations through a state machine for testing
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def device_id(self) -> str:
|
|
65
|
+
"""Unique device identifier."""
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
# === Screenshot ===
|
|
69
|
+
def get_screenshot(self, timeout: int = 10) -> Screenshot:
|
|
70
|
+
"""
|
|
71
|
+
Capture current screen.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
timeout: Timeout in seconds for the operation.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Screenshot object containing base64 data and dimensions.
|
|
78
|
+
"""
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
# === Input Operations ===
|
|
82
|
+
def tap(self, x: int, y: int, delay: float | None = None) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Tap at specified coordinates.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
x: X coordinate.
|
|
88
|
+
y: Y coordinate.
|
|
89
|
+
delay: Optional delay after tap in seconds.
|
|
90
|
+
"""
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
def double_tap(self, x: int, y: int, delay: float | None = None) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Double tap at specified coordinates.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
x: X coordinate.
|
|
99
|
+
y: Y coordinate.
|
|
100
|
+
delay: Optional delay after double tap in seconds.
|
|
101
|
+
"""
|
|
102
|
+
...
|
|
103
|
+
|
|
104
|
+
def long_press(
|
|
105
|
+
self, x: int, y: int, duration_ms: int = 3000, delay: float | None = None
|
|
106
|
+
) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Long press at specified coordinates.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
x: X coordinate.
|
|
112
|
+
y: Y coordinate.
|
|
113
|
+
duration_ms: Duration of press in milliseconds.
|
|
114
|
+
delay: Optional delay after long press in seconds.
|
|
115
|
+
"""
|
|
116
|
+
...
|
|
117
|
+
|
|
118
|
+
def swipe(
|
|
119
|
+
self,
|
|
120
|
+
start_x: int,
|
|
121
|
+
start_y: int,
|
|
122
|
+
end_x: int,
|
|
123
|
+
end_y: int,
|
|
124
|
+
duration_ms: int | None = None,
|
|
125
|
+
delay: float | None = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
"""
|
|
128
|
+
Swipe from start to end coordinates.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
start_x: Starting X coordinate.
|
|
132
|
+
start_y: Starting Y coordinate.
|
|
133
|
+
end_x: Ending X coordinate.
|
|
134
|
+
end_y: Ending Y coordinate.
|
|
135
|
+
duration_ms: Duration of swipe in milliseconds.
|
|
136
|
+
delay: Optional delay after swipe in seconds.
|
|
137
|
+
"""
|
|
138
|
+
...
|
|
139
|
+
|
|
140
|
+
def type_text(self, text: str) -> None:
|
|
141
|
+
"""
|
|
142
|
+
Type text into the currently focused input field.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
text: The text to type.
|
|
146
|
+
"""
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
def clear_text(self) -> None:
|
|
150
|
+
"""Clear text in the currently focused input field."""
|
|
151
|
+
...
|
|
152
|
+
|
|
153
|
+
# === Navigation ===
|
|
154
|
+
def back(self, delay: float | None = None) -> None:
|
|
155
|
+
"""
|
|
156
|
+
Press the back button.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
delay: Optional delay after pressing back in seconds.
|
|
160
|
+
"""
|
|
161
|
+
...
|
|
162
|
+
|
|
163
|
+
def home(self, delay: float | None = None) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Press the home button.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
delay: Optional delay after pressing home in seconds.
|
|
169
|
+
"""
|
|
170
|
+
...
|
|
171
|
+
|
|
172
|
+
def launch_app(self, app_name: str, delay: float | None = None) -> bool:
|
|
173
|
+
"""
|
|
174
|
+
Launch an app by name.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
app_name: The app name to launch.
|
|
178
|
+
delay: Optional delay after launching in seconds.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
True if app was launched successfully, False otherwise.
|
|
182
|
+
"""
|
|
183
|
+
...
|
|
184
|
+
|
|
185
|
+
# === State Query ===
|
|
186
|
+
def get_current_app(self) -> str:
|
|
187
|
+
"""
|
|
188
|
+
Get the currently focused app name.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
The app name if recognized, otherwise "System Home".
|
|
192
|
+
"""
|
|
193
|
+
...
|
|
194
|
+
|
|
195
|
+
# === Keyboard Management ===
|
|
196
|
+
def detect_and_set_adb_keyboard(self) -> str:
|
|
197
|
+
"""
|
|
198
|
+
Detect current keyboard and switch to ADB Keyboard if needed.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
The original keyboard IME identifier for later restoration.
|
|
202
|
+
"""
|
|
203
|
+
...
|
|
204
|
+
|
|
205
|
+
def restore_keyboard(self, ime: str) -> None:
|
|
206
|
+
"""
|
|
207
|
+
Restore the original keyboard IME.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
ime: The IME identifier to restore.
|
|
211
|
+
"""
|
|
212
|
+
...
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@runtime_checkable
|
|
216
|
+
class DeviceManagerProtocol(Protocol):
|
|
217
|
+
"""Device manager protocol - manages multiple devices."""
|
|
218
|
+
|
|
219
|
+
def list_devices(self) -> list[DeviceInfo]:
|
|
220
|
+
"""
|
|
221
|
+
List all available devices.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
List of DeviceInfo objects.
|
|
225
|
+
"""
|
|
226
|
+
...
|
|
227
|
+
|
|
228
|
+
def get_device(self, device_id: str) -> DeviceProtocol:
|
|
229
|
+
"""
|
|
230
|
+
Get a device instance by ID.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
device_id: The device ID.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
DeviceProtocol implementation for the device.
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
KeyError: If device not found.
|
|
240
|
+
"""
|
|
241
|
+
...
|
|
242
|
+
|
|
243
|
+
def connect(self, address: str, timeout: int = 10) -> tuple[bool, str]:
|
|
244
|
+
"""
|
|
245
|
+
Connect to a remote device.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
address: Device address (e.g., "192.168.1.100:5555").
|
|
249
|
+
timeout: Connection timeout in seconds.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Tuple of (success, message).
|
|
253
|
+
"""
|
|
254
|
+
...
|
|
255
|
+
|
|
256
|
+
def disconnect(self, device_id: str) -> tuple[bool, str]:
|
|
257
|
+
"""
|
|
258
|
+
Disconnect from a device.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
device_id: The device ID to disconnect.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Tuple of (success, message).
|
|
265
|
+
"""
|
|
266
|
+
...
|