autoglm-gui 1.4.1__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 (104) hide show
  1. AutoGLM_GUI/__init__.py +11 -0
  2. AutoGLM_GUI/__main__.py +26 -4
  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/screenshot.py +22 -1
  15. AutoGLM_GUI/adb_plus/serial.py +38 -20
  16. AutoGLM_GUI/adb_plus/touch.py +4 -9
  17. AutoGLM_GUI/agents/__init__.py +43 -12
  18. AutoGLM_GUI/agents/events.py +19 -0
  19. AutoGLM_GUI/agents/factory.py +31 -38
  20. AutoGLM_GUI/agents/glm/__init__.py +7 -0
  21. AutoGLM_GUI/agents/glm/agent.py +292 -0
  22. AutoGLM_GUI/agents/glm/message_builder.py +81 -0
  23. AutoGLM_GUI/agents/glm/parser.py +110 -0
  24. AutoGLM_GUI/agents/glm/prompts_en.py +77 -0
  25. AutoGLM_GUI/agents/glm/prompts_zh.py +75 -0
  26. AutoGLM_GUI/agents/mai/__init__.py +28 -0
  27. AutoGLM_GUI/agents/mai/agent.py +405 -0
  28. AutoGLM_GUI/agents/mai/parser.py +254 -0
  29. AutoGLM_GUI/agents/mai/prompts.py +103 -0
  30. AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
  31. AutoGLM_GUI/agents/protocols.py +12 -8
  32. AutoGLM_GUI/agents/stream_runner.py +188 -0
  33. AutoGLM_GUI/api/__init__.py +40 -21
  34. AutoGLM_GUI/api/agents.py +157 -240
  35. AutoGLM_GUI/api/control.py +9 -6
  36. AutoGLM_GUI/api/devices.py +102 -12
  37. AutoGLM_GUI/api/history.py +78 -0
  38. AutoGLM_GUI/api/layered_agent.py +67 -15
  39. AutoGLM_GUI/api/media.py +64 -1
  40. AutoGLM_GUI/api/scheduled_tasks.py +98 -0
  41. AutoGLM_GUI/config.py +81 -0
  42. AutoGLM_GUI/config_manager.py +68 -51
  43. AutoGLM_GUI/device_manager.py +248 -29
  44. AutoGLM_GUI/device_protocol.py +1 -1
  45. AutoGLM_GUI/devices/adb_device.py +5 -10
  46. AutoGLM_GUI/devices/mock_device.py +4 -2
  47. AutoGLM_GUI/devices/remote_device.py +8 -3
  48. AutoGLM_GUI/history_manager.py +164 -0
  49. AutoGLM_GUI/i18n.py +81 -0
  50. AutoGLM_GUI/model/__init__.py +5 -0
  51. AutoGLM_GUI/model/message_builder.py +69 -0
  52. AutoGLM_GUI/model/types.py +24 -0
  53. AutoGLM_GUI/models/__init__.py +10 -0
  54. AutoGLM_GUI/models/history.py +96 -0
  55. AutoGLM_GUI/models/scheduled_task.py +71 -0
  56. AutoGLM_GUI/parsers/__init__.py +22 -0
  57. AutoGLM_GUI/parsers/base.py +50 -0
  58. AutoGLM_GUI/parsers/phone_parser.py +58 -0
  59. AutoGLM_GUI/phone_agent_manager.py +62 -396
  60. AutoGLM_GUI/platform_utils.py +26 -0
  61. AutoGLM_GUI/prompt_config.py +15 -0
  62. AutoGLM_GUI/prompts/__init__.py +32 -0
  63. AutoGLM_GUI/scheduler_manager.py +304 -0
  64. AutoGLM_GUI/schemas.py +234 -72
  65. AutoGLM_GUI/scrcpy_stream.py +142 -24
  66. AutoGLM_GUI/socketio_server.py +100 -27
  67. AutoGLM_GUI/static/assets/{about-_XNhzQZX.js → about-BQm96DAl.js} +1 -1
  68. AutoGLM_GUI/static/assets/alert-dialog-B42XxGPR.js +1 -0
  69. AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +129 -0
  70. AutoGLM_GUI/static/assets/circle-alert-D4rSJh37.js +1 -0
  71. AutoGLM_GUI/static/assets/dialog-DZ78cEcj.js +45 -0
  72. AutoGLM_GUI/static/assets/history-DFBv7TGc.js +1 -0
  73. AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +1 -0
  74. AutoGLM_GUI/static/assets/{index-Cy8TmmHV.js → index-CmZSnDqc.js} +1 -1
  75. AutoGLM_GUI/static/assets/index-CssG-3TH.js +11 -0
  76. AutoGLM_GUI/static/assets/label-BCUzE_nm.js +1 -0
  77. AutoGLM_GUI/static/assets/logs-eoFxn5of.js +1 -0
  78. AutoGLM_GUI/static/assets/popover-DLsuV5Sx.js +1 -0
  79. AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +1 -0
  80. AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +1 -0
  81. AutoGLM_GUI/static/assets/textarea-BX6y7uM5.js +1 -0
  82. AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +1 -0
  83. AutoGLM_GUI/static/index.html +2 -2
  84. AutoGLM_GUI/types.py +17 -0
  85. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/METADATA +137 -130
  86. autoglm_gui-1.5.0.dist-info/RECORD +157 -0
  87. AutoGLM_GUI/agents/mai_adapter.py +0 -627
  88. AutoGLM_GUI/api/dual_model.py +0 -317
  89. AutoGLM_GUI/dual_model/__init__.py +0 -53
  90. AutoGLM_GUI/dual_model/decision_model.py +0 -664
  91. AutoGLM_GUI/dual_model/dual_agent.py +0 -917
  92. AutoGLM_GUI/dual_model/protocols.py +0 -354
  93. AutoGLM_GUI/dual_model/vision_model.py +0 -442
  94. AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
  95. AutoGLM_GUI/phone_agent_patches.py +0 -147
  96. AutoGLM_GUI/static/assets/chat-DwJpiAWf.js +0 -126
  97. AutoGLM_GUI/static/assets/dialog-B3uW4T8V.js +0 -45
  98. AutoGLM_GUI/static/assets/index-Cpv2gSF1.css +0 -1
  99. AutoGLM_GUI/static/assets/index-UYYauTly.js +0 -12
  100. AutoGLM_GUI/static/assets/workflows-Du_de-dt.js +0 -1
  101. autoglm_gui-1.4.1.dist-info/RECORD +0 -117
  102. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/WHEEL +0 -0
  103. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/entry_points.txt +0 -0
  104. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -52,12 +52,6 @@ class ConfigModel(BaseModel):
52
52
  model_name: str = "autoglm-phone-9b"
53
53
  api_key: str = "EMPTY"
54
54
 
55
- # 双模型配置
56
- dual_model_enabled: bool = False
57
- decision_base_url: str = ""
58
- decision_model_name: str = ""
59
- decision_api_key: str = ""
60
-
61
55
  # Agent 类型配置
62
56
  agent_type: str = "glm" # Agent type (e.g., "glm", "mai")
63
57
  agent_config_params: dict | None = None # Agent-specific configuration
@@ -65,6 +59,11 @@ class ConfigModel(BaseModel):
65
59
  # Agent 执行配置
66
60
  default_max_steps: int = 100 # 单次任务最大执行步数
67
61
 
62
+ # 决策模型配置(用于分层代理)
63
+ decision_base_url: str | None = None
64
+ decision_model_name: str | None = None
65
+ decision_api_key: str | None = None
66
+
68
67
  @field_validator("default_max_steps")
69
68
  @classmethod
70
69
  def validate_default_max_steps(cls, v: int) -> int:
@@ -93,11 +92,23 @@ class ConfigModel(BaseModel):
93
92
 
94
93
  @field_validator("decision_base_url")
95
94
  @classmethod
96
- def validate_decision_base_url(cls, v: str) -> str:
95
+ def validate_decision_base_url(cls, v: str | None) -> str | None:
97
96
  """验证 decision_base_url 格式."""
98
- if v and not v.startswith(("http://", "https://")):
99
- raise ValueError("decision_base_url must start with http:// or https://")
100
- return v.rstrip("/") # 去除尾部斜杠
97
+ if v is not None:
98
+ if not v.startswith(("http://", "https://")):
99
+ raise ValueError(
100
+ "decision_base_url must start with http:// or https://"
101
+ )
102
+ return v.rstrip("/")
103
+ return v
104
+
105
+ @field_validator("decision_model_name")
106
+ @classmethod
107
+ def validate_decision_model_name(cls, v: str | None) -> str | None:
108
+ """验证 decision_model_name 非空."""
109
+ if v is not None and (not v or not v.strip()):
110
+ raise ValueError("decision_model_name cannot be empty string")
111
+ return v.strip() if v else v
101
112
 
102
113
 
103
114
  # ==================== 配置层数据类 ====================
@@ -110,16 +121,15 @@ class ConfigLayer:
110
121
  base_url: Optional[str] = None
111
122
  model_name: Optional[str] = None
112
123
  api_key: Optional[str] = None
113
- # 双模型配置
114
- dual_model_enabled: Optional[bool] = None
115
- decision_base_url: Optional[str] = None
116
- decision_model_name: Optional[str] = None
117
- decision_api_key: Optional[str] = None
118
124
  # Agent 类型配置
119
125
  agent_type: Optional[str] = None
120
126
  agent_config_params: Optional[dict] = None
121
127
  # Agent 执行配置
122
128
  default_max_steps: Optional[int] = None
129
+ # 决策模型配置
130
+ decision_base_url: Optional[str] = None
131
+ decision_model_name: Optional[str] = None
132
+ decision_api_key: Optional[str] = None
123
133
 
124
134
  source: ConfigSource = ConfigSource.DEFAULT
125
135
 
@@ -147,13 +157,12 @@ class ConfigLayer:
147
157
  "base_url": self.base_url,
148
158
  "model_name": self.model_name,
149
159
  "api_key": self.api_key,
150
- "dual_model_enabled": self.dual_model_enabled,
151
- "decision_base_url": self.decision_base_url,
152
- "decision_model_name": self.decision_model_name,
153
- "decision_api_key": self.decision_api_key,
154
160
  "agent_type": self.agent_type,
155
161
  "agent_config_params": self.agent_config_params,
156
162
  "default_max_steps": self.default_max_steps,
163
+ "decision_base_url": self.decision_base_url,
164
+ "decision_model_name": self.decision_model_name,
165
+ "decision_api_key": self.decision_api_key,
157
166
  }.items()
158
167
  if v is not None
159
168
  }
@@ -216,6 +225,9 @@ class UnifiedConfigManager:
216
225
  agent_type="glm",
217
226
  agent_config_params=None,
218
227
  default_max_steps=100,
228
+ decision_base_url=None,
229
+ decision_model_name=None,
230
+ decision_api_key=None,
219
231
  source=ConfigSource.DEFAULT,
220
232
  )
221
233
 
@@ -262,15 +274,26 @@ class UnifiedConfigManager:
262
274
  - AUTOGLM_BASE_URL
263
275
  - AUTOGLM_MODEL_NAME
264
276
  - AUTOGLM_API_KEY
277
+ - AUTOGLM_DECISION_BASE_URL
278
+ - AUTOGLM_DECISION_MODEL_NAME
279
+ - AUTOGLM_DECISION_API_KEY
265
280
  """
266
281
  base_url = os.getenv("AUTOGLM_BASE_URL")
267
282
  model_name = os.getenv("AUTOGLM_MODEL_NAME")
268
283
  api_key = os.getenv("AUTOGLM_API_KEY")
269
284
 
285
+ # 决策模型环境变量
286
+ decision_base_url = os.getenv("AUTOGLM_DECISION_BASE_URL")
287
+ decision_model_name = os.getenv("AUTOGLM_DECISION_MODEL_NAME")
288
+ decision_api_key = os.getenv("AUTOGLM_DECISION_API_KEY")
289
+
270
290
  self._env_layer = ConfigLayer(
271
291
  base_url=base_url if base_url else None,
272
292
  model_name=model_name if model_name else None,
273
293
  api_key=api_key if api_key else None,
294
+ decision_base_url=decision_base_url if decision_base_url else None,
295
+ decision_model_name=decision_model_name if decision_model_name else None,
296
+ decision_api_key=decision_api_key if decision_api_key else None,
274
297
  source=ConfigSource.ENV,
275
298
  )
276
299
  self._effective_config = None # 清除缓存
@@ -324,15 +347,14 @@ class UnifiedConfigManager:
324
347
  base_url=config_data.get("base_url"),
325
348
  model_name=config_data.get("model_name"),
326
349
  api_key=config_data.get("api_key"),
327
- dual_model_enabled=config_data.get("dual_model_enabled"),
328
- decision_base_url=config_data.get("decision_base_url"),
329
- decision_model_name=config_data.get("decision_model_name"),
330
- decision_api_key=config_data.get("decision_api_key"),
331
350
  agent_type=config_data.get(
332
351
  "agent_type", "glm"
333
352
  ), # 默认 'glm',兼容旧配置
334
353
  agent_config_params=config_data.get("agent_config_params"),
335
354
  default_max_steps=config_data.get("default_max_steps"),
355
+ decision_base_url=config_data.get("decision_base_url"),
356
+ decision_model_name=config_data.get("decision_model_name"),
357
+ decision_api_key=config_data.get("decision_api_key"),
336
358
  source=ConfigSource.FILE,
337
359
  )
338
360
  self._effective_config = None # 清除缓存
@@ -360,13 +382,12 @@ class UnifiedConfigManager:
360
382
  base_url: str,
361
383
  model_name: str,
362
384
  api_key: Optional[str] = None,
363
- dual_model_enabled: Optional[bool] = None,
364
- decision_base_url: Optional[str] = None,
365
- decision_model_name: Optional[str] = None,
366
- decision_api_key: Optional[str] = None,
367
385
  agent_type: Optional[str] = None,
368
386
  agent_config_params: Optional[dict] = None,
369
387
  default_max_steps: Optional[int] = None,
388
+ decision_base_url: Optional[str] = None,
389
+ decision_model_name: Optional[str] = None,
390
+ decision_api_key: Optional[str] = None,
370
391
  merge_mode: bool = True,
371
392
  ) -> bool:
372
393
  """
@@ -376,13 +397,12 @@ class UnifiedConfigManager:
376
397
  base_url: Base URL
377
398
  model_name: 模型名称
378
399
  api_key: API key(可选)
379
- dual_model_enabled: 是否启用双模型
380
- decision_base_url: 决策模型 Base URL
381
- decision_model_name: 决策模型名称
382
- decision_api_key: 决策模型 API key
383
400
  agent_type: Agent 类型(可选,如 "glm", "mai")
384
401
  agent_config_params: Agent 特定配置参数(可选)
385
402
  default_max_steps: 默认最大执行步数(可选)
403
+ decision_base_url: 决策模型 Base URL(可选)
404
+ decision_model_name: 决策模型名称(可选)
405
+ decision_api_key: 决策模型 API Key(可选)
386
406
  merge_mode: 是否合并现有配置(True: 保留未提供的字段)
387
407
 
388
408
  Returns:
@@ -400,14 +420,6 @@ class UnifiedConfigManager:
400
420
 
401
421
  if api_key:
402
422
  new_config["api_key"] = api_key
403
- if dual_model_enabled is not None:
404
- new_config["dual_model_enabled"] = dual_model_enabled
405
- if decision_base_url:
406
- new_config["decision_base_url"] = decision_base_url
407
- if decision_model_name:
408
- new_config["decision_model_name"] = decision_model_name
409
- if decision_api_key:
410
- new_config["decision_api_key"] = decision_api_key
411
423
  if agent_type is not None:
412
424
  new_config["agent_type"] = agent_type
413
425
  if agent_config_params is not None:
@@ -415,6 +427,14 @@ class UnifiedConfigManager:
415
427
  if default_max_steps is not None:
416
428
  new_config["default_max_steps"] = default_max_steps
417
429
 
430
+ # 决策模型配置
431
+ if decision_base_url is not None:
432
+ new_config["decision_base_url"] = decision_base_url
433
+ if decision_model_name is not None:
434
+ new_config["decision_model_name"] = decision_model_name
435
+ if decision_api_key is not None:
436
+ new_config["decision_api_key"] = decision_api_key
437
+
418
438
  # 合并模式:保留现有文件中未提供的字段
419
439
  if merge_mode and self._config_path.exists():
420
440
  try:
@@ -424,13 +444,12 @@ class UnifiedConfigManager:
424
444
  # 保留未提供的字段
425
445
  preserve_keys = [
426
446
  "api_key",
427
- "dual_model_enabled",
428
- "decision_base_url",
429
- "decision_model_name",
430
- "decision_api_key",
431
447
  "agent_type",
432
448
  "agent_config_params",
433
449
  "default_max_steps",
450
+ "decision_base_url",
451
+ "decision_model_name",
452
+ "decision_api_key",
434
453
  ]
435
454
  for key in preserve_keys:
436
455
  if key not in new_config and key in existing:
@@ -515,13 +534,12 @@ class UnifiedConfigManager:
515
534
  "base_url",
516
535
  "model_name",
517
536
  "api_key",
518
- "dual_model_enabled",
519
- "decision_base_url",
520
- "decision_model_name",
521
- "decision_api_key",
522
537
  "agent_type",
523
538
  "agent_config_params",
524
539
  "default_max_steps",
540
+ "decision_base_url",
541
+ "decision_model_name",
542
+ "decision_api_key",
525
543
  ]
526
544
 
527
545
  for key in config_keys:
@@ -684,13 +702,12 @@ class UnifiedConfigManager:
684
702
  "base_url": config.base_url,
685
703
  "model_name": config.model_name,
686
704
  "api_key": config.api_key,
687
- "dual_model_enabled": config.dual_model_enabled,
688
- "decision_base_url": config.decision_base_url,
689
- "decision_model_name": config.decision_model_name,
690
- "decision_api_key": config.decision_api_key,
691
705
  "agent_type": config.agent_type,
692
706
  "agent_config_params": config.agent_config_params,
693
707
  "default_max_steps": config.default_max_steps,
708
+ "decision_base_url": config.decision_base_url,
709
+ "decision_model_name": config.decision_model_name,
710
+ "decision_api_key": config.decision_api_key,
694
711
  }
695
712
 
696
713
 
@@ -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)
@@ -42,7 +42,7 @@ class DeviceInfo:
42
42
  status: str # "online" | "offline" | "unauthorized"
43
43
  model: str | None = None
44
44
  platform: str = "android" # "android" | "ios" | "harmonyos"
45
- connection_type: str = "usb" # "usb" | "wifi" | "remote"
45
+ connection_type: str = "usb" # "usb" | "wifi" (ADB WiFi) | "remote" (HTTP Remote)
46
46
 
47
47
 
48
48
  @runtime_checkable
@@ -1,12 +1,7 @@
1
- """ADB Device implementation of DeviceProtocol.
2
-
3
- This module wraps the existing phone_agent.adb module to provide
4
- a DeviceProtocol-compliant implementation.
5
- """
6
-
7
- from phone_agent import adb
8
- from phone_agent.adb import ADBConnection
1
+ """ADB Device implementation of DeviceProtocol."""
9
2
 
3
+ from AutoGLM_GUI import adb
4
+ from AutoGLM_GUI.adb import ADBConnection
10
5
  from AutoGLM_GUI.device_protocol import (
11
6
  DeviceInfo,
12
7
  DeviceManagerProtocol,
@@ -15,7 +10,7 @@ from AutoGLM_GUI.device_protocol import (
15
10
  )
16
11
 
17
12
 
18
- class ADBDevice:
13
+ class ADBDevice(DeviceProtocol):
19
14
  """
20
15
  ADB device implementation using local subprocess calls.
21
16
 
@@ -121,7 +116,7 @@ class ADBDevice:
121
116
  assert isinstance(ADBDevice("test"), DeviceProtocol)
122
117
 
123
118
 
124
- class ADBDeviceManager:
119
+ class ADBDeviceManager(DeviceManagerProtocol):
125
120
  """
126
121
  ADB device manager implementation.
127
122