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.
- AutoGLM_GUI/__init__.py +11 -0
- AutoGLM_GUI/__main__.py +26 -4
- 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/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 +43 -12
- AutoGLM_GUI/agents/events.py +19 -0
- AutoGLM_GUI/agents/factory.py +31 -38
- 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 +12 -8
- AutoGLM_GUI/agents/stream_runner.py +188 -0
- AutoGLM_GUI/api/__init__.py +40 -21
- AutoGLM_GUI/api/agents.py +157 -240
- AutoGLM_GUI/api/control.py +9 -6
- AutoGLM_GUI/api/devices.py +102 -12
- AutoGLM_GUI/api/history.py +78 -0
- AutoGLM_GUI/api/layered_agent.py +67 -15
- AutoGLM_GUI/api/media.py +64 -1
- AutoGLM_GUI/api/scheduled_tasks.py +98 -0
- AutoGLM_GUI/config.py +81 -0
- AutoGLM_GUI/config_manager.py +68 -51
- AutoGLM_GUI/device_manager.py +248 -29
- AutoGLM_GUI/device_protocol.py +1 -1
- AutoGLM_GUI/devices/adb_device.py +5 -10
- AutoGLM_GUI/devices/mock_device.py +4 -2
- AutoGLM_GUI/devices/remote_device.py +8 -3
- AutoGLM_GUI/history_manager.py +164 -0
- AutoGLM_GUI/i18n.py +81 -0
- 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 +62 -396
- AutoGLM_GUI/platform_utils.py +26 -0
- 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 +234 -72
- AutoGLM_GUI/scrcpy_stream.py +142 -24
- AutoGLM_GUI/socketio_server.py +100 -27
- AutoGLM_GUI/static/assets/{about-_XNhzQZX.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-Cy8TmmHV.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 +17 -0
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/METADATA +137 -130
- autoglm_gui-1.5.0.dist-info/RECORD +157 -0
- AutoGLM_GUI/agents/mai_adapter.py +0 -627
- AutoGLM_GUI/api/dual_model.py +0 -317
- 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 -147
- AutoGLM_GUI/static/assets/chat-DwJpiAWf.js +0 -126
- AutoGLM_GUI/static/assets/dialog-B3uW4T8V.js +0 -45
- AutoGLM_GUI/static/assets/index-Cpv2gSF1.css +0 -1
- AutoGLM_GUI/static/assets/index-UYYauTly.js +0 -12
- AutoGLM_GUI/static/assets/workflows-Du_de-dt.js +0 -1
- autoglm_gui-1.4.1.dist-info/RECORD +0 -117
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/config_manager.py
CHANGED
|
@@ -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
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
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)
|
AutoGLM_GUI/device_protocol.py
CHANGED
|
@@ -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
|
|