autoglm-gui 1.4.0__py3-none-any.whl → 1.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.
- AutoGLM_GUI/__init__.py +11 -0
- AutoGLM_GUI/__main__.py +26 -8
- AutoGLM_GUI/actions/__init__.py +6 -0
- AutoGLM_GUI/actions/handler.py +196 -0
- AutoGLM_GUI/actions/types.py +15 -0
- AutoGLM_GUI/adb/__init__.py +53 -0
- AutoGLM_GUI/adb/apps.py +227 -0
- AutoGLM_GUI/adb/connection.py +323 -0
- AutoGLM_GUI/adb/device.py +171 -0
- AutoGLM_GUI/adb/input.py +67 -0
- AutoGLM_GUI/adb/screenshot.py +11 -0
- AutoGLM_GUI/adb/timing.py +167 -0
- AutoGLM_GUI/adb_plus/keyboard_installer.py +4 -2
- AutoGLM_GUI/adb_plus/qr_pair.py +8 -8
- AutoGLM_GUI/adb_plus/screenshot.py +22 -1
- AutoGLM_GUI/adb_plus/serial.py +38 -20
- AutoGLM_GUI/adb_plus/touch.py +4 -9
- AutoGLM_GUI/agents/__init__.py +51 -0
- AutoGLM_GUI/agents/events.py +19 -0
- AutoGLM_GUI/agents/factory.py +153 -0
- AutoGLM_GUI/agents/glm/__init__.py +7 -0
- AutoGLM_GUI/agents/glm/agent.py +292 -0
- AutoGLM_GUI/agents/glm/message_builder.py +81 -0
- AutoGLM_GUI/agents/glm/parser.py +110 -0
- AutoGLM_GUI/agents/glm/prompts_en.py +77 -0
- AutoGLM_GUI/agents/glm/prompts_zh.py +75 -0
- AutoGLM_GUI/agents/mai/__init__.py +28 -0
- AutoGLM_GUI/agents/mai/agent.py +405 -0
- AutoGLM_GUI/agents/mai/parser.py +254 -0
- AutoGLM_GUI/agents/mai/prompts.py +103 -0
- AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
- AutoGLM_GUI/agents/protocols.py +27 -0
- AutoGLM_GUI/agents/stream_runner.py +188 -0
- AutoGLM_GUI/api/__init__.py +71 -11
- AutoGLM_GUI/api/agents.py +190 -229
- AutoGLM_GUI/api/control.py +9 -6
- AutoGLM_GUI/api/devices.py +112 -28
- AutoGLM_GUI/api/health.py +13 -0
- AutoGLM_GUI/api/history.py +78 -0
- AutoGLM_GUI/api/layered_agent.py +306 -181
- AutoGLM_GUI/api/mcp.py +11 -10
- AutoGLM_GUI/api/media.py +64 -1
- AutoGLM_GUI/api/scheduled_tasks.py +98 -0
- AutoGLM_GUI/api/version.py +23 -10
- AutoGLM_GUI/api/workflows.py +2 -1
- AutoGLM_GUI/config.py +72 -14
- AutoGLM_GUI/config_manager.py +98 -27
- AutoGLM_GUI/device_adapter.py +263 -0
- AutoGLM_GUI/device_manager.py +248 -29
- AutoGLM_GUI/device_protocol.py +266 -0
- AutoGLM_GUI/devices/__init__.py +49 -0
- AutoGLM_GUI/devices/adb_device.py +200 -0
- AutoGLM_GUI/devices/mock_device.py +185 -0
- AutoGLM_GUI/devices/remote_device.py +177 -0
- AutoGLM_GUI/exceptions.py +3 -3
- AutoGLM_GUI/history_manager.py +164 -0
- AutoGLM_GUI/i18n.py +81 -0
- AutoGLM_GUI/metrics.py +13 -20
- AutoGLM_GUI/model/__init__.py +5 -0
- AutoGLM_GUI/model/message_builder.py +69 -0
- AutoGLM_GUI/model/types.py +24 -0
- AutoGLM_GUI/models/__init__.py +10 -0
- AutoGLM_GUI/models/history.py +96 -0
- AutoGLM_GUI/models/scheduled_task.py +71 -0
- AutoGLM_GUI/parsers/__init__.py +22 -0
- AutoGLM_GUI/parsers/base.py +50 -0
- AutoGLM_GUI/parsers/phone_parser.py +58 -0
- AutoGLM_GUI/phone_agent_manager.py +118 -367
- AutoGLM_GUI/platform_utils.py +31 -2
- AutoGLM_GUI/prompt_config.py +15 -0
- AutoGLM_GUI/prompts/__init__.py +32 -0
- AutoGLM_GUI/scheduler_manager.py +304 -0
- AutoGLM_GUI/schemas.py +272 -63
- AutoGLM_GUI/scrcpy_stream.py +159 -37
- AutoGLM_GUI/server.py +3 -1
- AutoGLM_GUI/socketio_server.py +114 -29
- AutoGLM_GUI/state.py +10 -30
- AutoGLM_GUI/static/assets/{about-DeclntHg.js → about-BQm96DAl.js} +1 -1
- AutoGLM_GUI/static/assets/alert-dialog-B42XxGPR.js +1 -0
- AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +129 -0
- AutoGLM_GUI/static/assets/circle-alert-D4rSJh37.js +1 -0
- AutoGLM_GUI/static/assets/dialog-DZ78cEcj.js +45 -0
- AutoGLM_GUI/static/assets/history-DFBv7TGc.js +1 -0
- AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +1 -0
- AutoGLM_GUI/static/assets/{index-zQ4KKDHt.js → index-CmZSnDqc.js} +1 -1
- AutoGLM_GUI/static/assets/index-CssG-3TH.js +11 -0
- AutoGLM_GUI/static/assets/label-BCUzE_nm.js +1 -0
- AutoGLM_GUI/static/assets/logs-eoFxn5of.js +1 -0
- AutoGLM_GUI/static/assets/popover-DLsuV5Sx.js +1 -0
- AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +1 -0
- AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +1 -0
- AutoGLM_GUI/static/assets/textarea-BX6y7uM5.js +1 -0
- AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +1 -0
- AutoGLM_GUI/static/index.html +2 -2
- AutoGLM_GUI/types.py +142 -0
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/METADATA +178 -92
- autoglm_gui-1.5.0.dist-info/RECORD +157 -0
- 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/api/dual_model.py +0 -311
- AutoGLM_GUI/dual_model/__init__.py +0 -53
- AutoGLM_GUI/dual_model/decision_model.py +0 -664
- AutoGLM_GUI/dual_model/dual_agent.py +0 -917
- AutoGLM_GUI/dual_model/protocols.py +0 -354
- AutoGLM_GUI/dual_model/vision_model.py +0 -442
- AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
- AutoGLM_GUI/phone_agent_patches.py +0 -146
- AutoGLM_GUI/static/assets/chat-Iut2yhSw.js +0 -125
- AutoGLM_GUI/static/assets/dialog-BfdcBs1x.js +0 -45
- AutoGLM_GUI/static/assets/index-5hCCwHA7.css +0 -1
- AutoGLM_GUI/static/assets/index-DHF1NZh0.js +0 -12
- AutoGLM_GUI/static/assets/workflows-xiplap-r.js +0 -1
- autoglm_gui-1.4.0.dist-info/RECORD +0 -100
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
AutoGLM_GUI/device_manager.py
CHANGED
|
@@ -7,11 +7,30 @@ import time
|
|
|
7
7
|
from collections import defaultdict
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
9
|
from enum import Enum
|
|
10
|
-
from typing import Optional
|
|
11
|
-
|
|
12
|
-
from phone_agent.adb.connection import ADBConnection, ConnectionType, DeviceInfo
|
|
10
|
+
from typing import TYPE_CHECKING, Optional
|
|
13
11
|
|
|
12
|
+
from AutoGLM_GUI.adb import ADBConnection, ConnectionType, DeviceInfo
|
|
14
13
|
from AutoGLM_GUI.logger import logger
|
|
14
|
+
from AutoGLM_GUI.types import DeviceConnectionType
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from AutoGLM_GUI.device_protocol import DeviceProtocol
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def convert_connection_type(ct: ConnectionType) -> DeviceConnectionType:
|
|
21
|
+
"""Convert phone_agent ConnectionType to DeviceConnectionType.
|
|
22
|
+
|
|
23
|
+
phone_agent.ConnectionType.REMOTE is actually WiFi ADB,
|
|
24
|
+
so we map it to DeviceConnectionType.WIFI.
|
|
25
|
+
"""
|
|
26
|
+
if ct == ConnectionType.USB:
|
|
27
|
+
return DeviceConnectionType.USB
|
|
28
|
+
elif ct == ConnectionType.WIFI:
|
|
29
|
+
return DeviceConnectionType.WIFI
|
|
30
|
+
elif ct == ConnectionType.REMOTE:
|
|
31
|
+
return DeviceConnectionType.WIFI
|
|
32
|
+
else:
|
|
33
|
+
return DeviceConnectionType.USB
|
|
15
34
|
|
|
16
35
|
|
|
17
36
|
class DeviceState(str, Enum):
|
|
@@ -28,7 +47,7 @@ class DeviceConnection:
|
|
|
28
47
|
"""Single connection method for a device (USB, WiFi, mDNS, etc.)."""
|
|
29
48
|
|
|
30
49
|
device_id: str # USB serial OR IP:port
|
|
31
|
-
connection_type:
|
|
50
|
+
connection_type: DeviceConnectionType
|
|
32
51
|
status: str # "device" | "offline" | "unauthorized"
|
|
33
52
|
last_seen: float = field(default_factory=time.time)
|
|
34
53
|
|
|
@@ -36,14 +55,13 @@ class DeviceConnection:
|
|
|
36
55
|
"""Calculate connection priority for sorting.
|
|
37
56
|
|
|
38
57
|
Priority:
|
|
39
|
-
1. Connection type (USB > WiFi
|
|
58
|
+
1. Connection type (USB > WiFi > Remote)
|
|
40
59
|
2. Status (device > offline > unauthorized)
|
|
41
60
|
"""
|
|
42
|
-
# Type priority (higher is better)
|
|
43
61
|
type_priority = {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
62
|
+
DeviceConnectionType.USB: 300,
|
|
63
|
+
DeviceConnectionType.WIFI: 200,
|
|
64
|
+
DeviceConnectionType.REMOTE: 100,
|
|
47
65
|
}
|
|
48
66
|
|
|
49
67
|
# Status priority
|
|
@@ -98,7 +116,7 @@ class ManagedDevice:
|
|
|
98
116
|
return self.primary_connection.status
|
|
99
117
|
|
|
100
118
|
@property
|
|
101
|
-
def connection_type(self) ->
|
|
119
|
+
def connection_type(self) -> DeviceConnectionType:
|
|
102
120
|
"""Type of primary connection."""
|
|
103
121
|
return self.primary_connection.connection_type
|
|
104
122
|
|
|
@@ -153,7 +171,7 @@ def _create_managed_device(
|
|
|
153
171
|
connections = [
|
|
154
172
|
DeviceConnection(
|
|
155
173
|
device_id=d.device_id,
|
|
156
|
-
connection_type=d.connection_type,
|
|
174
|
+
connection_type=convert_connection_type(d.connection_type),
|
|
157
175
|
status=d.status,
|
|
158
176
|
last_seen=time.time(),
|
|
159
177
|
)
|
|
@@ -228,6 +246,9 @@ class DeviceManager:
|
|
|
228
246
|
self._mdns_devices: dict[str, ManagedDevice] = {} # Key: serial
|
|
229
247
|
self._enable_mdns_discovery: bool = True # Feature toggle
|
|
230
248
|
|
|
249
|
+
self._remote_devices: dict[str, "DeviceProtocol"] = {}
|
|
250
|
+
self._remote_device_configs: dict[str, dict] = {}
|
|
251
|
+
|
|
231
252
|
@classmethod
|
|
232
253
|
def get_instance(cls, adb_path: str = "adb") -> DeviceManager:
|
|
233
254
|
"""Get singleton instance (thread-safe)."""
|
|
@@ -312,9 +333,19 @@ class DeviceManager:
|
|
|
312
333
|
return None
|
|
313
334
|
|
|
314
335
|
def force_refresh(self) -> None:
|
|
315
|
-
"""Trigger immediate device list refresh (blocking).
|
|
336
|
+
"""Trigger immediate device list refresh (blocking).
|
|
337
|
+
|
|
338
|
+
Note: This method may fail if ADB is unavailable. Exceptions are logged
|
|
339
|
+
but not propagated to support remote-only deployments.
|
|
340
|
+
"""
|
|
316
341
|
logger.info("Force refreshing device list...")
|
|
317
|
-
|
|
342
|
+
try:
|
|
343
|
+
self._poll_devices()
|
|
344
|
+
except Exception as e:
|
|
345
|
+
logger.warning(
|
|
346
|
+
f"Device poll failed during force refresh: {e}. "
|
|
347
|
+
f"This is expected in remote-only deployments without local ADB."
|
|
348
|
+
)
|
|
318
349
|
|
|
319
350
|
# Internal methods
|
|
320
351
|
|
|
@@ -366,16 +397,8 @@ class DeviceManager:
|
|
|
366
397
|
device_with_serials: list[tuple[DeviceInfo, str]] = []
|
|
367
398
|
|
|
368
399
|
for device_info in adb_devices:
|
|
400
|
+
# get_device_serial always returns a value (uses device_id as fallback)
|
|
369
401
|
serial = get_device_serial(device_info.device_id, self._adb_path)
|
|
370
|
-
|
|
371
|
-
if not serial:
|
|
372
|
-
# CRITICAL: Log error and skip this device
|
|
373
|
-
logger.error(
|
|
374
|
-
f"Failed to get serial for device {device_info.device_id}. "
|
|
375
|
-
f"Skipping this device. Check ADB access."
|
|
376
|
-
)
|
|
377
|
-
continue
|
|
378
|
-
|
|
379
402
|
device_with_serials.append((device_info, serial))
|
|
380
403
|
|
|
381
404
|
# Step 2: Group devices by serial
|
|
@@ -414,6 +437,12 @@ class DeviceManager:
|
|
|
414
437
|
|
|
415
438
|
added_serials = current_serials - previous_serials
|
|
416
439
|
removed_serials = previous_serials - current_serials
|
|
440
|
+
removed_serials = {
|
|
441
|
+
s
|
|
442
|
+
for s in removed_serials
|
|
443
|
+
if s not in self._devices
|
|
444
|
+
or self._devices[s].connection_type != DeviceConnectionType.REMOTE
|
|
445
|
+
}
|
|
417
446
|
existing_serials = current_serials & previous_serials
|
|
418
447
|
|
|
419
448
|
# Add new devices
|
|
@@ -441,7 +470,7 @@ class DeviceManager:
|
|
|
441
470
|
new_connections = [
|
|
442
471
|
DeviceConnection(
|
|
443
472
|
device_id=d.device_id,
|
|
444
|
-
connection_type=d.connection_type,
|
|
473
|
+
connection_type=convert_connection_type(d.connection_type),
|
|
445
474
|
status=d.status,
|
|
446
475
|
last_seen=time.time(),
|
|
447
476
|
)
|
|
@@ -531,8 +560,8 @@ class DeviceManager:
|
|
|
531
560
|
connections=[
|
|
532
561
|
DeviceConnection(
|
|
533
562
|
device_id=f"{mdns_dev.ip}:{mdns_dev.port}",
|
|
534
|
-
connection_type=
|
|
535
|
-
status="available",
|
|
563
|
+
connection_type=DeviceConnectionType.WIFI,
|
|
564
|
+
status="available",
|
|
536
565
|
last_seen=time.time(),
|
|
537
566
|
)
|
|
538
567
|
],
|
|
@@ -590,7 +619,7 @@ class DeviceManager:
|
|
|
590
619
|
Returns:
|
|
591
620
|
Tuple of (success, message, wifi_device_id)
|
|
592
621
|
"""
|
|
593
|
-
from
|
|
622
|
+
from AutoGLM_GUI.adb import ADBConnection, ConnectionType
|
|
594
623
|
|
|
595
624
|
from AutoGLM_GUI.adb_plus import get_wifi_ip
|
|
596
625
|
|
|
@@ -637,7 +666,7 @@ class DeviceManager:
|
|
|
637
666
|
Returns:
|
|
638
667
|
Tuple of (success, message)
|
|
639
668
|
"""
|
|
640
|
-
from
|
|
669
|
+
from AutoGLM_GUI.adb import ADBConnection
|
|
641
670
|
|
|
642
671
|
conn = ADBConnection(adb_path=self._adb_path)
|
|
643
672
|
ok, msg = conn.disconnect(device_id)
|
|
@@ -663,7 +692,7 @@ class DeviceManager:
|
|
|
663
692
|
"""
|
|
664
693
|
import re
|
|
665
694
|
|
|
666
|
-
from
|
|
695
|
+
from AutoGLM_GUI.adb import ADBConnection
|
|
667
696
|
|
|
668
697
|
# IP format validation
|
|
669
698
|
ip_pattern = r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$"
|
|
@@ -701,7 +730,7 @@ class DeviceManager:
|
|
|
701
730
|
"""
|
|
702
731
|
import re
|
|
703
732
|
|
|
704
|
-
from
|
|
733
|
+
from AutoGLM_GUI.adb import ADBConnection
|
|
705
734
|
|
|
706
735
|
from AutoGLM_GUI.adb_plus import pair_device
|
|
707
736
|
|
|
@@ -758,3 +787,193 @@ class DeviceManager:
|
|
|
758
787
|
f"Successfully paired and connected to {connection_address}",
|
|
759
788
|
connection_address,
|
|
760
789
|
)
|
|
790
|
+
|
|
791
|
+
def discover_remote_devices(
|
|
792
|
+
self, base_url: str, timeout: int = 5
|
|
793
|
+
) -> tuple[bool, str, list[dict]]:
|
|
794
|
+
"""Discover devices from a remote Device Agent Server.
|
|
795
|
+
|
|
796
|
+
Args:
|
|
797
|
+
base_url: Remote Agent Server address
|
|
798
|
+
timeout: Connection timeout in seconds
|
|
799
|
+
|
|
800
|
+
Returns:
|
|
801
|
+
Tuple of (success, message, devices_list)
|
|
802
|
+
"""
|
|
803
|
+
from AutoGLM_GUI.devices.remote_device import RemoteDeviceManager
|
|
804
|
+
|
|
805
|
+
base_url = base_url.strip().rstrip("/")
|
|
806
|
+
if not base_url.startswith(("http://", "https://")):
|
|
807
|
+
return (False, "base_url must start with http:// or https://", [])
|
|
808
|
+
|
|
809
|
+
try:
|
|
810
|
+
remote_manager = RemoteDeviceManager(base_url, timeout=float(timeout))
|
|
811
|
+
devices = remote_manager.list_devices()
|
|
812
|
+
|
|
813
|
+
devices_list = [
|
|
814
|
+
{
|
|
815
|
+
"device_id": d.device_id,
|
|
816
|
+
"model": d.model or "Unknown",
|
|
817
|
+
"platform": d.platform,
|
|
818
|
+
"status": d.status,
|
|
819
|
+
}
|
|
820
|
+
for d in devices
|
|
821
|
+
]
|
|
822
|
+
|
|
823
|
+
return (True, f"Found {len(devices_list)} device(s)", devices_list)
|
|
824
|
+
except Exception as e:
|
|
825
|
+
logger.error(f"Failed to discover remote devices: {e}")
|
|
826
|
+
return (False, f"Discovery failed: {str(e)}", [])
|
|
827
|
+
|
|
828
|
+
def add_remote_device(self, base_url: str, device_id: str) -> tuple[bool, str, str]:
|
|
829
|
+
"""Manually add a remote HTTP proxy device.
|
|
830
|
+
|
|
831
|
+
Args:
|
|
832
|
+
base_url: Remote Agent Server address (e.g., http://server:8001)
|
|
833
|
+
device_id: Device ID on the remote server
|
|
834
|
+
|
|
835
|
+
Returns:
|
|
836
|
+
Tuple of (success, message, synthetic_serial)
|
|
837
|
+
"""
|
|
838
|
+
from AutoGLM_GUI.devices.remote_device import RemoteDevice
|
|
839
|
+
|
|
840
|
+
base_url = base_url.strip().rstrip("/")
|
|
841
|
+
if not base_url.startswith(("http://", "https://")):
|
|
842
|
+
return (False, "base_url must start with http:// or https://", "")
|
|
843
|
+
|
|
844
|
+
synthetic_serial = f"remote:{base_url}:{device_id}"
|
|
845
|
+
|
|
846
|
+
with self._devices_lock:
|
|
847
|
+
if synthetic_serial in self._devices:
|
|
848
|
+
return (False, f"Remote device {device_id} already exists", "")
|
|
849
|
+
|
|
850
|
+
try:
|
|
851
|
+
remote_device = RemoteDevice(device_id, base_url)
|
|
852
|
+
remote_device.get_screenshot(timeout=5)
|
|
853
|
+
|
|
854
|
+
managed = ManagedDevice(
|
|
855
|
+
serial=synthetic_serial,
|
|
856
|
+
connections=[
|
|
857
|
+
DeviceConnection(
|
|
858
|
+
device_id=f"{base_url}|{device_id}",
|
|
859
|
+
connection_type=DeviceConnectionType.REMOTE,
|
|
860
|
+
status="device",
|
|
861
|
+
last_seen=time.time(),
|
|
862
|
+
)
|
|
863
|
+
],
|
|
864
|
+
model=device_id,
|
|
865
|
+
state=DeviceState.ONLINE,
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
self._devices[synthetic_serial] = managed
|
|
869
|
+
self._remote_devices[synthetic_serial] = remote_device
|
|
870
|
+
self._remote_device_configs[synthetic_serial] = {
|
|
871
|
+
"base_url": base_url,
|
|
872
|
+
"device_id": device_id,
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
self._device_id_to_serial[managed.primary_device_id] = synthetic_serial
|
|
876
|
+
|
|
877
|
+
logger.info(f"Remote device added: {synthetic_serial}")
|
|
878
|
+
return (True, "Remote device added successfully", synthetic_serial)
|
|
879
|
+
|
|
880
|
+
except Exception as e:
|
|
881
|
+
logger.error(f"Failed to connect to remote device: {e}")
|
|
882
|
+
return (False, f"Connection failed: {str(e)}", "")
|
|
883
|
+
|
|
884
|
+
def remove_remote_device(self, serial: str) -> tuple[bool, str]:
|
|
885
|
+
"""Remove a remote device.
|
|
886
|
+
|
|
887
|
+
Args:
|
|
888
|
+
serial: Synthetic serial of the remote device (remote:...)
|
|
889
|
+
|
|
890
|
+
Returns:
|
|
891
|
+
Tuple of (success, message)
|
|
892
|
+
"""
|
|
893
|
+
with self._devices_lock:
|
|
894
|
+
if serial not in self._devices:
|
|
895
|
+
return (False, "Remote device not found")
|
|
896
|
+
|
|
897
|
+
managed = self._devices.get(serial)
|
|
898
|
+
if not managed or managed.connection_type != DeviceConnectionType.REMOTE:
|
|
899
|
+
return (False, "Not a remote device")
|
|
900
|
+
|
|
901
|
+
managed = self._devices.pop(serial)
|
|
902
|
+
remote_device = self._remote_devices.pop(serial, None)
|
|
903
|
+
self._remote_device_configs.pop(serial, None)
|
|
904
|
+
|
|
905
|
+
for conn in managed.connections:
|
|
906
|
+
self._device_id_to_serial.pop(conn.device_id, None)
|
|
907
|
+
|
|
908
|
+
if remote_device:
|
|
909
|
+
try:
|
|
910
|
+
remote_device.close() # type: ignore
|
|
911
|
+
except Exception as e:
|
|
912
|
+
logger.warning(f"Error closing remote device: {e}")
|
|
913
|
+
|
|
914
|
+
logger.info(f"Remote device removed: {serial}")
|
|
915
|
+
return (True, "Remote device removed successfully")
|
|
916
|
+
|
|
917
|
+
def get_remote_device_instance(self, serial: str) -> "DeviceProtocol | None":
|
|
918
|
+
"""Get RemoteDevice instance for device adapter injection.
|
|
919
|
+
|
|
920
|
+
Args:
|
|
921
|
+
serial: Synthetic serial of the remote device
|
|
922
|
+
|
|
923
|
+
Returns:
|
|
924
|
+
RemoteDevice instance or None if not found
|
|
925
|
+
"""
|
|
926
|
+
return self._remote_devices.get(serial)
|
|
927
|
+
|
|
928
|
+
def get_serial_by_device_id(self, device_id: str) -> str | None:
|
|
929
|
+
"""Get serial by device_id (reverse lookup).
|
|
930
|
+
|
|
931
|
+
Args:
|
|
932
|
+
device_id: Device ID from connections
|
|
933
|
+
|
|
934
|
+
Returns:
|
|
935
|
+
Serial (synthetic or ADB) or None if not found
|
|
936
|
+
"""
|
|
937
|
+
return self._device_id_to_serial.get(device_id)
|
|
938
|
+
|
|
939
|
+
def get_device_protocol(self, device_id: str) -> "DeviceProtocol":
|
|
940
|
+
"""
|
|
941
|
+
根据 device_id 获取 DeviceProtocol 实例(统一入口).
|
|
942
|
+
|
|
943
|
+
自动识别设备类型(ADB / Remote)并返回对应的实现。
|
|
944
|
+
|
|
945
|
+
Args:
|
|
946
|
+
device_id: 设备标识符(USB serial / IP:port / remote_xxx)
|
|
947
|
+
|
|
948
|
+
Returns:
|
|
949
|
+
DeviceProtocol 实例(ADBDevice 或 RemoteDevice)
|
|
950
|
+
|
|
951
|
+
Raises:
|
|
952
|
+
ValueError: 设备未找到或不可用
|
|
953
|
+
|
|
954
|
+
Example:
|
|
955
|
+
>>> manager = DeviceManager.get_instance()
|
|
956
|
+
>>> device = manager.get_device_protocol("192.168.1.100:5555")
|
|
957
|
+
>>> screenshot = device.get_screenshot() # 不关心是 ADB 还是 Remote
|
|
958
|
+
"""
|
|
959
|
+
with self._devices_lock:
|
|
960
|
+
# 1. 查找设备元数据
|
|
961
|
+
managed = self.get_device_by_device_id(device_id)
|
|
962
|
+
if not managed:
|
|
963
|
+
raise ValueError(f"Device {device_id} not found in DeviceManager")
|
|
964
|
+
|
|
965
|
+
# 2. 根据连接类型返回对应实现
|
|
966
|
+
if managed.connection_type == DeviceConnectionType.REMOTE:
|
|
967
|
+
# Remote device: 返回 HTTP 客户端
|
|
968
|
+
remote_device = self.get_remote_device_instance(managed.serial)
|
|
969
|
+
if not remote_device:
|
|
970
|
+
raise ValueError(
|
|
971
|
+
f"Remote device instance not found for serial {managed.serial}"
|
|
972
|
+
)
|
|
973
|
+
return remote_device # type: ignore[return-value]
|
|
974
|
+
|
|
975
|
+
else:
|
|
976
|
+
# ADB device (USB / WiFi): 返回本地 ADB 包装
|
|
977
|
+
from AutoGLM_GUI.devices.adb_device import ADBDevice
|
|
978
|
+
|
|
979
|
+
return ADBDevice(managed.primary_device_id)
|