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.
Files changed (120) hide show
  1. AutoGLM_GUI/__init__.py +11 -0
  2. AutoGLM_GUI/__main__.py +26 -8
  3. AutoGLM_GUI/actions/__init__.py +6 -0
  4. AutoGLM_GUI/actions/handler.py +196 -0
  5. AutoGLM_GUI/actions/types.py +15 -0
  6. AutoGLM_GUI/adb/__init__.py +53 -0
  7. AutoGLM_GUI/adb/apps.py +227 -0
  8. AutoGLM_GUI/adb/connection.py +323 -0
  9. AutoGLM_GUI/adb/device.py +171 -0
  10. AutoGLM_GUI/adb/input.py +67 -0
  11. AutoGLM_GUI/adb/screenshot.py +11 -0
  12. AutoGLM_GUI/adb/timing.py +167 -0
  13. AutoGLM_GUI/adb_plus/keyboard_installer.py +4 -2
  14. AutoGLM_GUI/adb_plus/qr_pair.py +8 -8
  15. AutoGLM_GUI/adb_plus/screenshot.py +22 -1
  16. AutoGLM_GUI/adb_plus/serial.py +38 -20
  17. AutoGLM_GUI/adb_plus/touch.py +4 -9
  18. AutoGLM_GUI/agents/__init__.py +51 -0
  19. AutoGLM_GUI/agents/events.py +19 -0
  20. AutoGLM_GUI/agents/factory.py +153 -0
  21. AutoGLM_GUI/agents/glm/__init__.py +7 -0
  22. AutoGLM_GUI/agents/glm/agent.py +292 -0
  23. AutoGLM_GUI/agents/glm/message_builder.py +81 -0
  24. AutoGLM_GUI/agents/glm/parser.py +110 -0
  25. AutoGLM_GUI/agents/glm/prompts_en.py +77 -0
  26. AutoGLM_GUI/agents/glm/prompts_zh.py +75 -0
  27. AutoGLM_GUI/agents/mai/__init__.py +28 -0
  28. AutoGLM_GUI/agents/mai/agent.py +405 -0
  29. AutoGLM_GUI/agents/mai/parser.py +254 -0
  30. AutoGLM_GUI/agents/mai/prompts.py +103 -0
  31. AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
  32. AutoGLM_GUI/agents/protocols.py +27 -0
  33. AutoGLM_GUI/agents/stream_runner.py +188 -0
  34. AutoGLM_GUI/api/__init__.py +71 -11
  35. AutoGLM_GUI/api/agents.py +190 -229
  36. AutoGLM_GUI/api/control.py +9 -6
  37. AutoGLM_GUI/api/devices.py +112 -28
  38. AutoGLM_GUI/api/health.py +13 -0
  39. AutoGLM_GUI/api/history.py +78 -0
  40. AutoGLM_GUI/api/layered_agent.py +306 -181
  41. AutoGLM_GUI/api/mcp.py +11 -10
  42. AutoGLM_GUI/api/media.py +64 -1
  43. AutoGLM_GUI/api/scheduled_tasks.py +98 -0
  44. AutoGLM_GUI/api/version.py +23 -10
  45. AutoGLM_GUI/api/workflows.py +2 -1
  46. AutoGLM_GUI/config.py +72 -14
  47. AutoGLM_GUI/config_manager.py +98 -27
  48. AutoGLM_GUI/device_adapter.py +263 -0
  49. AutoGLM_GUI/device_manager.py +248 -29
  50. AutoGLM_GUI/device_protocol.py +266 -0
  51. AutoGLM_GUI/devices/__init__.py +49 -0
  52. AutoGLM_GUI/devices/adb_device.py +200 -0
  53. AutoGLM_GUI/devices/mock_device.py +185 -0
  54. AutoGLM_GUI/devices/remote_device.py +177 -0
  55. AutoGLM_GUI/exceptions.py +3 -3
  56. AutoGLM_GUI/history_manager.py +164 -0
  57. AutoGLM_GUI/i18n.py +81 -0
  58. AutoGLM_GUI/metrics.py +13 -20
  59. AutoGLM_GUI/model/__init__.py +5 -0
  60. AutoGLM_GUI/model/message_builder.py +69 -0
  61. AutoGLM_GUI/model/types.py +24 -0
  62. AutoGLM_GUI/models/__init__.py +10 -0
  63. AutoGLM_GUI/models/history.py +96 -0
  64. AutoGLM_GUI/models/scheduled_task.py +71 -0
  65. AutoGLM_GUI/parsers/__init__.py +22 -0
  66. AutoGLM_GUI/parsers/base.py +50 -0
  67. AutoGLM_GUI/parsers/phone_parser.py +58 -0
  68. AutoGLM_GUI/phone_agent_manager.py +118 -367
  69. AutoGLM_GUI/platform_utils.py +31 -2
  70. AutoGLM_GUI/prompt_config.py +15 -0
  71. AutoGLM_GUI/prompts/__init__.py +32 -0
  72. AutoGLM_GUI/scheduler_manager.py +304 -0
  73. AutoGLM_GUI/schemas.py +272 -63
  74. AutoGLM_GUI/scrcpy_stream.py +159 -37
  75. AutoGLM_GUI/server.py +3 -1
  76. AutoGLM_GUI/socketio_server.py +114 -29
  77. AutoGLM_GUI/state.py +10 -30
  78. AutoGLM_GUI/static/assets/{about-DeclntHg.js → about-BQm96DAl.js} +1 -1
  79. AutoGLM_GUI/static/assets/alert-dialog-B42XxGPR.js +1 -0
  80. AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +129 -0
  81. AutoGLM_GUI/static/assets/circle-alert-D4rSJh37.js +1 -0
  82. AutoGLM_GUI/static/assets/dialog-DZ78cEcj.js +45 -0
  83. AutoGLM_GUI/static/assets/history-DFBv7TGc.js +1 -0
  84. AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +1 -0
  85. AutoGLM_GUI/static/assets/{index-zQ4KKDHt.js → index-CmZSnDqc.js} +1 -1
  86. AutoGLM_GUI/static/assets/index-CssG-3TH.js +11 -0
  87. AutoGLM_GUI/static/assets/label-BCUzE_nm.js +1 -0
  88. AutoGLM_GUI/static/assets/logs-eoFxn5of.js +1 -0
  89. AutoGLM_GUI/static/assets/popover-DLsuV5Sx.js +1 -0
  90. AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +1 -0
  91. AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +1 -0
  92. AutoGLM_GUI/static/assets/textarea-BX6y7uM5.js +1 -0
  93. AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +1 -0
  94. AutoGLM_GUI/static/index.html +2 -2
  95. AutoGLM_GUI/types.py +142 -0
  96. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/METADATA +178 -92
  97. autoglm_gui-1.5.0.dist-info/RECORD +157 -0
  98. mai_agent/base.py +137 -0
  99. mai_agent/mai_grounding_agent.py +263 -0
  100. mai_agent/mai_naivigation_agent.py +526 -0
  101. mai_agent/prompt.py +148 -0
  102. mai_agent/unified_memory.py +67 -0
  103. mai_agent/utils.py +73 -0
  104. AutoGLM_GUI/api/dual_model.py +0 -311
  105. AutoGLM_GUI/dual_model/__init__.py +0 -53
  106. AutoGLM_GUI/dual_model/decision_model.py +0 -664
  107. AutoGLM_GUI/dual_model/dual_agent.py +0 -917
  108. AutoGLM_GUI/dual_model/protocols.py +0 -354
  109. AutoGLM_GUI/dual_model/vision_model.py +0 -442
  110. AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
  111. AutoGLM_GUI/phone_agent_patches.py +0 -146
  112. AutoGLM_GUI/static/assets/chat-Iut2yhSw.js +0 -125
  113. AutoGLM_GUI/static/assets/dialog-BfdcBs1x.js +0 -45
  114. AutoGLM_GUI/static/assets/index-5hCCwHA7.css +0 -1
  115. AutoGLM_GUI/static/assets/index-DHF1NZh0.js +0 -12
  116. AutoGLM_GUI/static/assets/workflows-xiplap-r.js +0 -1
  117. autoglm_gui-1.4.0.dist-info/RECORD +0 -100
  118. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/WHEEL +0 -0
  119. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/entry_points.txt +0 -0
  120. {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
@@ -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: ConnectionType
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/Remote > mDNS)
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
- ConnectionType.USB: 300,
45
- ConnectionType.WIFI: 200,
46
- ConnectionType.REMOTE: 200,
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) -> ConnectionType:
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
- self._poll_devices()
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=ConnectionType.REMOTE,
535
- status="available", # Not connected yet
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 phone_agent.adb.connection import ADBConnection, ConnectionType
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 phone_agent.adb.connection import ADBConnection
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 phone_agent.adb.connection import ADBConnection
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 phone_agent.adb.connection import ADBConnection
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)