autoglm-gui 1.0.2__py3-none-any.whl → 1.2.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 (38) hide show
  1. AutoGLM_GUI/adb_plus/__init__.py +12 -1
  2. AutoGLM_GUI/adb_plus/mdns.py +192 -0
  3. AutoGLM_GUI/adb_plus/pair.py +60 -0
  4. AutoGLM_GUI/adb_plus/qr_pair.py +372 -0
  5. AutoGLM_GUI/adb_plus/serial.py +61 -2
  6. AutoGLM_GUI/adb_plus/version.py +81 -0
  7. AutoGLM_GUI/api/__init__.py +16 -1
  8. AutoGLM_GUI/api/agents.py +329 -94
  9. AutoGLM_GUI/api/devices.py +304 -100
  10. AutoGLM_GUI/api/workflows.py +70 -0
  11. AutoGLM_GUI/device_manager.py +760 -0
  12. AutoGLM_GUI/exceptions.py +18 -0
  13. AutoGLM_GUI/phone_agent_manager.py +549 -0
  14. AutoGLM_GUI/phone_agent_patches.py +146 -0
  15. AutoGLM_GUI/schemas.py +380 -2
  16. AutoGLM_GUI/state.py +21 -0
  17. AutoGLM_GUI/static/assets/{about-BOnRPlKQ.js → about-PcGX7dIG.js} +1 -1
  18. AutoGLM_GUI/static/assets/chat-B0FKL2ne.js +124 -0
  19. AutoGLM_GUI/static/assets/dialog-BSNX0L1i.js +45 -0
  20. AutoGLM_GUI/static/assets/index-BjYIY--m.css +1 -0
  21. AutoGLM_GUI/static/assets/index-CnEYDOXp.js +11 -0
  22. AutoGLM_GUI/static/assets/index-DOt5XNhh.js +1 -0
  23. AutoGLM_GUI/static/assets/logo-Cyfm06Ym.png +0 -0
  24. AutoGLM_GUI/static/assets/workflows-B1hgBC_O.js +1 -0
  25. AutoGLM_GUI/static/favicon.ico +0 -0
  26. AutoGLM_GUI/static/index.html +9 -2
  27. AutoGLM_GUI/static/logo-192.png +0 -0
  28. AutoGLM_GUI/static/logo-512.png +0 -0
  29. AutoGLM_GUI/workflow_manager.py +181 -0
  30. {autoglm_gui-1.0.2.dist-info → autoglm_gui-1.2.0.dist-info}/METADATA +80 -35
  31. {autoglm_gui-1.0.2.dist-info → autoglm_gui-1.2.0.dist-info}/RECORD +34 -19
  32. AutoGLM_GUI/static/assets/chat-CGW6uMKB.js +0 -149
  33. AutoGLM_GUI/static/assets/index-CRFVU0eu.js +0 -1
  34. AutoGLM_GUI/static/assets/index-DH-Dl4tK.js +0 -10
  35. AutoGLM_GUI/static/assets/index-DzUQ89YC.css +0 -1
  36. {autoglm_gui-1.0.2.dist-info → autoglm_gui-1.2.0.dist-info}/WHEEL +0 -0
  37. {autoglm_gui-1.0.2.dist-info → autoglm_gui-1.2.0.dist-info}/entry_points.txt +0 -0
  38. {autoglm_gui-1.0.2.dist-info → autoglm_gui-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,17 @@
1
1
  """Device discovery routes."""
2
2
 
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
3
7
  from fastapi import APIRouter
4
8
 
5
- from AutoGLM_GUI.adb_plus import get_wifi_ip, get_device_serial
9
+ if TYPE_CHECKING:
10
+ from AutoGLM_GUI.device_manager import ManagedDevice
11
+ from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
12
+
13
+ from AutoGLM_GUI.adb_plus.qr_pair import qr_pairing_manager
14
+ from AutoGLM_GUI.logger import logger
6
15
 
7
16
  from AutoGLM_GUI.schemas import (
8
17
  DeviceListResponse,
@@ -12,112 +21,134 @@ from AutoGLM_GUI.schemas import (
12
21
  WiFiDisconnectResponse,
13
22
  WiFiManualConnectRequest,
14
23
  WiFiManualConnectResponse,
24
+ WiFiPairRequest,
25
+ WiFiPairResponse,
26
+ MdnsDiscoverResponse,
27
+ MdnsDeviceResponse,
28
+ QRPairGenerateResponse,
29
+ QRPairStatusResponse,
30
+ QRPairCancelResponse,
15
31
  )
16
- from AutoGLM_GUI.state import agents
32
+
33
+
34
+ def _build_device_response_with_agent(
35
+ device: ManagedDevice, agent_manager: PhoneAgentManager
36
+ ) -> dict:
37
+ """组合设备信息和 Agent 状态(API 层职责)。
38
+
39
+ Args:
40
+ device: ManagedDevice 实例
41
+ agent_manager: PhoneAgentManager 实例
42
+
43
+ Returns:
44
+ dict: 完整的设备响应,匹配 DeviceResponse schema
45
+ """
46
+ # 获取纯设备信息
47
+ response = device.to_dict()
48
+
49
+ # 通过 serial 查找 Agent(支持连接切换)
50
+ agent_device_id = agent_manager.find_agent_by_serial(device.serial)
51
+
52
+ if agent_device_id:
53
+ metadata = agent_manager.get_metadata(agent_device_id)
54
+
55
+ if metadata:
56
+ response["agent"] = {
57
+ "state": metadata.state.value,
58
+ "created_at": metadata.created_at,
59
+ "last_used": metadata.last_used,
60
+ "error_message": metadata.error_message,
61
+ "model_name": metadata.model_config.model_name,
62
+ }
63
+ else:
64
+ response["agent"] = None
65
+ else:
66
+ response["agent"] = None
67
+
68
+ return response
69
+
17
70
 
18
71
  router = APIRouter()
19
72
 
20
73
 
21
74
  @router.get("/api/devices", response_model=DeviceListResponse)
22
75
  def list_devices() -> DeviceListResponse:
23
- """列出所有 ADB 设备。"""
24
- from phone_agent.adb import list_devices as adb_list, ADBConnection
25
-
26
- adb_devices = adb_list()
27
- conn = ADBConnection()
28
-
29
- devices_with_serial = []
30
- for d in adb_devices:
31
- # 使用 adb_plus 的 get_device_serial 获取真实序列号
32
- serial = get_device_serial(d.device_id, conn.adb_path)
33
-
34
- devices_with_serial.append(
35
- {
36
- "id": d.device_id,
37
- "model": d.model or "Unknown",
38
- "status": d.status,
39
- "connection_type": d.connection_type.value,
40
- "is_initialized": d.device_id in agents,
41
- "serial": serial, # 真实序列号
42
- }
43
- )
76
+ """列出所有 ADB 设备及 Agent 状态."""
77
+ from AutoGLM_GUI.device_manager import DeviceManager
78
+ from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
79
+
80
+ device_manager = DeviceManager.get_instance()
81
+ agent_manager = PhoneAgentManager.get_instance()
44
82
 
45
- return DeviceListResponse(devices=devices_with_serial)
83
+ # Fallback: 如果轮询未启动,执行同步获取
84
+ if not device_manager._poll_thread or not device_manager._poll_thread.is_alive():
85
+ logger.warning("Polling not started, performing synchronous device fetch")
86
+ device_manager.force_refresh()
87
+
88
+ managed_devices = device_manager.get_devices()
89
+
90
+ # API 层负责聚合设备信息和 Agent 状态
91
+ devices_with_agents = [
92
+ _build_device_response_with_agent(d, agent_manager) for d in managed_devices
93
+ ]
94
+
95
+ return DeviceListResponse(devices=devices_with_agents)
46
96
 
47
97
 
48
98
  @router.post("/api/devices/connect_wifi", response_model=WiFiConnectResponse)
49
99
  def connect_wifi(request: WiFiConnectRequest) -> WiFiConnectResponse:
50
100
  """从 USB 启用 TCP/IP 并连接到 WiFi。"""
51
- from phone_agent.adb import ADBConnection, ConnectionType
101
+ from AutoGLM_GUI.device_manager import DeviceManager
52
102
 
53
- conn = ADBConnection()
103
+ device_manager = DeviceManager.get_instance()
104
+ success, message, wifi_id = device_manager.connect_wifi(
105
+ device_id=request.device_id,
106
+ port=request.port,
107
+ )
54
108
 
55
- # 优先使用传入的 device_id,否则取第一个在线设备
56
- device_info = conn.get_device_info(request.device_id)
57
- if not device_info:
58
- return WiFiConnectResponse(
59
- success=False,
60
- message="No connected device found",
61
- error="device_not_found",
62
- )
109
+ if success:
110
+ # Immediately refresh device list to show new WiFi device
111
+ device_manager.force_refresh()
63
112
 
64
- # 已经是 WiFi 连接则直接返回
65
- if device_info.connection_type == ConnectionType.REMOTE:
66
- address = device_info.device_id
67
113
  return WiFiConnectResponse(
68
114
  success=True,
69
- message="Already connected over WiFi",
70
- device_id=address,
71
- address=address,
72
- )
73
-
74
- # 1) 启用 tcpip
75
- ok, msg = conn.enable_tcpip(port=request.port, device_id=device_info.device_id)
76
- if not ok:
77
- return WiFiConnectResponse(
78
- success=False, message=msg or "Failed to enable tcpip", error="tcpip"
115
+ message=message,
116
+ device_id=wifi_id,
117
+ address=wifi_id,
79
118
  )
119
+ else:
120
+ # Determine error type from message
121
+ error_type = "connect"
122
+ if "not found" in message.lower():
123
+ error_type = "device_not_found"
124
+ elif "tcpip" in message.lower():
125
+ error_type = "tcpip"
126
+ elif "ip" in message.lower():
127
+ error_type = "ip"
80
128
 
81
- # 2) 读取设备 IP:先用本地 adb_plus 的 WiFi 优先逻辑,失败再回退上游接口
82
- ip = get_wifi_ip(conn.adb_path, device_info.device_id) or conn.get_device_ip(
83
- device_info.device_id
84
- )
85
- if not ip:
86
- return WiFiConnectResponse(
87
- success=False, message="Failed to get device IP", error="ip"
88
- )
89
-
90
- address = f"{ip}:{request.port}"
91
-
92
- # 3) 连接 WiFi
93
- ok, msg = conn.connect(address)
94
- if not ok:
95
129
  return WiFiConnectResponse(
96
130
  success=False,
97
- message=msg or "Failed to connect over WiFi",
98
- error="connect",
131
+ message=message,
132
+ error=error_type,
99
133
  )
100
134
 
101
- return WiFiConnectResponse(
102
- success=True,
103
- message="Switched to WiFi successfully",
104
- device_id=address,
105
- address=address,
106
- )
107
-
108
135
 
109
136
  @router.post("/api/devices/disconnect_wifi", response_model=WiFiDisconnectResponse)
110
137
  def disconnect_wifi(request: WiFiDisconnectRequest) -> WiFiDisconnectResponse:
111
138
  """断开 WiFi 连接。"""
112
- from phone_agent.adb import ADBConnection
139
+ from AutoGLM_GUI.device_manager import DeviceManager
140
+
141
+ device_manager = DeviceManager.get_instance()
142
+ success, message = device_manager.disconnect_wifi(request.device_id)
113
143
 
114
- conn = ADBConnection()
115
- ok, msg = conn.disconnect(request.device_id)
144
+ if success:
145
+ # Refresh device list to update status
146
+ device_manager.force_refresh()
116
147
 
117
148
  return WiFiDisconnectResponse(
118
- success=ok,
119
- message=msg,
120
- error=None if ok else "disconnect_failed",
149
+ success=success,
150
+ message=message,
151
+ error=None if success else "disconnect_failed",
121
152
  )
122
153
 
123
154
 
@@ -128,41 +159,214 @@ def connect_wifi_manual(
128
159
  request: WiFiManualConnectRequest,
129
160
  ) -> WiFiManualConnectResponse:
130
161
  """手动连接到 WiFi 设备 (直接连接,无需 USB)."""
131
- import re
162
+ from AutoGLM_GUI.device_manager import DeviceManager
132
163
 
133
- from phone_agent.adb import ADBConnection
164
+ device_manager = DeviceManager.get_instance()
165
+ success, message, device_id = device_manager.connect_wifi_manual(
166
+ ip=request.ip,
167
+ port=request.port,
168
+ )
169
+
170
+ if success:
171
+ # Refresh device list to show new device
172
+ device_manager.force_refresh()
134
173
 
135
- # IP 格式验证
136
- ip_pattern = r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$"
137
- if not re.match(ip_pattern, request.ip):
138
174
  return WiFiManualConnectResponse(
139
- success=False,
140
- message="Invalid IP address format",
141
- error="invalid_ip",
175
+ success=True,
176
+ message=message,
177
+ device_id=device_id,
142
178
  )
179
+ else:
180
+ # Determine error type from message
181
+ error_type = "connect_failed"
182
+ if "Invalid IP" in message:
183
+ error_type = "invalid_ip"
184
+ elif "Port must be" in message:
185
+ error_type = "invalid_port"
143
186
 
144
- # 端口范围验证
145
- if not (1 <= request.port <= 65535):
146
187
  return WiFiManualConnectResponse(
147
188
  success=False,
148
- message="Port must be between 1 and 65535",
149
- error="invalid_port",
189
+ message=message,
190
+ error=error_type,
150
191
  )
151
192
 
152
- conn = ADBConnection()
153
- address = f"{request.ip}:{request.port}"
154
193
 
155
- # 直接连接
156
- ok, msg = conn.connect(address)
157
- if not ok:
158
- return WiFiManualConnectResponse(
194
+ @router.post("/api/devices/pair_wifi", response_model=WiFiPairResponse)
195
+ def pair_wifi(request: WiFiPairRequest) -> WiFiPairResponse:
196
+ """使用无线调试配对并连接到 WiFi 设备 (Android 11+)."""
197
+ from AutoGLM_GUI.device_manager import DeviceManager
198
+
199
+ device_manager = DeviceManager.get_instance()
200
+ success, message, device_id = device_manager.pair_wifi(
201
+ ip=request.ip,
202
+ pairing_port=request.pairing_port,
203
+ pairing_code=request.pairing_code,
204
+ connection_port=request.connection_port,
205
+ )
206
+
207
+ if success:
208
+ # Refresh device list to show newly paired device
209
+ device_manager.force_refresh()
210
+
211
+ return WiFiPairResponse(
212
+ success=True,
213
+ message=message,
214
+ device_id=device_id,
215
+ )
216
+ else:
217
+ # Determine error type from message
218
+ error_type = "connect_failed"
219
+ if "Invalid IP" in message:
220
+ error_type = "invalid_ip"
221
+ elif "port must be" in message.lower():
222
+ error_type = "invalid_port"
223
+ elif "Pairing code must be" in message:
224
+ error_type = "invalid_pairing_code"
225
+ elif "connection failed" not in message.lower():
226
+ error_type = "pair_failed"
227
+
228
+ return WiFiPairResponse(
159
229
  success=False,
160
- message=msg or f"Failed to connect to {address}",
161
- error="connect_failed",
230
+ message=message,
231
+ error=error_type,
232
+ )
233
+
234
+
235
+ @router.get("/api/devices/discover_mdns", response_model=MdnsDiscoverResponse)
236
+ def discover_mdns() -> MdnsDiscoverResponse:
237
+ """Discover wireless ADB devices via mDNS."""
238
+ from phone_agent.adb import ADBConnection
239
+ from AutoGLM_GUI.adb_plus import discover_mdns_devices
240
+
241
+ try:
242
+ conn = ADBConnection()
243
+ devices = discover_mdns_devices(conn.adb_path)
244
+
245
+ device_responses = [
246
+ MdnsDeviceResponse(
247
+ name=dev.name,
248
+ ip=dev.ip,
249
+ port=dev.port,
250
+ has_pairing=dev.has_pairing,
251
+ service_type=dev.service_type,
252
+ pairing_port=dev.pairing_port,
253
+ )
254
+ for dev in devices
255
+ ]
256
+
257
+ return MdnsDiscoverResponse(
258
+ success=True,
259
+ devices=device_responses,
260
+ )
261
+
262
+ except Exception as e:
263
+ return MdnsDiscoverResponse(
264
+ success=False,
265
+ devices=[],
266
+ error=str(e),
267
+ )
268
+
269
+
270
+ # QR Code Pairing Routes
271
+
272
+
273
+ @router.post("/api/devices/qr_pair/generate", response_model=QRPairGenerateResponse)
274
+ def generate_qr_pairing(timeout: int = 90) -> QRPairGenerateResponse:
275
+ """Generate QR code for wireless pairing and start mDNS listener.
276
+
277
+ Args:
278
+ timeout: Session timeout in seconds (default 90)
279
+
280
+ Returns:
281
+ QR code payload and session information
282
+ """
283
+ try:
284
+ from phone_agent.adb import ADBConnection
285
+
286
+ conn = ADBConnection()
287
+ session = qr_pairing_manager.create_session(
288
+ timeout=timeout, adb_path=conn.adb_path
162
289
  )
163
290
 
164
- return WiFiManualConnectResponse(
165
- success=True,
166
- message=f"Successfully connected to {address}",
167
- device_id=address,
291
+ return QRPairGenerateResponse(
292
+ success=True,
293
+ qr_payload=session.qr_payload,
294
+ session_id=session.session_id,
295
+ expires_at=session.expires_at,
296
+ message="QR code generated, listening for devices...",
297
+ )
298
+ except Exception as e:
299
+ return QRPairGenerateResponse(
300
+ success=False,
301
+ message=f"Failed to generate QR pairing: {str(e)}",
302
+ error="generation_failed",
303
+ )
304
+
305
+
306
+ def _get_status_message(status: str) -> str:
307
+ """Get user-friendly message for status code."""
308
+ messages = {
309
+ "listening": "等待手机扫描二维码...",
310
+ "pairing": "正在配对设备...",
311
+ "paired": "配对成功,正在连接...",
312
+ "connecting": "正在建立连接...",
313
+ "connected": "连接成功!",
314
+ "timeout": "超时:未检测到设备扫码",
315
+ "error": "配对失败",
316
+ }
317
+ return messages.get(status, "未知状态")
318
+
319
+
320
+ @router.get(
321
+ "/api/devices/qr_pair/status/{session_id}", response_model=QRPairStatusResponse
322
+ )
323
+ def get_qr_pairing_status(session_id: str) -> QRPairStatusResponse:
324
+ """Get current status of a QR pairing session.
325
+
326
+ Args:
327
+ session_id: Session UUID
328
+
329
+ Returns:
330
+ Current session status and device information if connected
331
+ """
332
+ session = qr_pairing_manager.get_session(session_id)
333
+
334
+ if not session:
335
+ return QRPairStatusResponse(
336
+ session_id=session_id,
337
+ status="error",
338
+ message="Session not found or expired",
339
+ error="session_not_found",
340
+ )
341
+
342
+ return QRPairStatusResponse(
343
+ session_id=session.session_id,
344
+ status=session.status,
345
+ device_id=session.device_id,
346
+ message=_get_status_message(session.status),
347
+ error=session.error_message,
168
348
  )
349
+
350
+
351
+ @router.delete("/api/devices/qr_pair/{session_id}", response_model=QRPairCancelResponse)
352
+ def cancel_qr_pairing(session_id: str) -> QRPairCancelResponse:
353
+ """Cancel an active QR pairing session.
354
+
355
+ Args:
356
+ session_id: Session UUID to cancel
357
+
358
+ Returns:
359
+ Success status
360
+ """
361
+ success = qr_pairing_manager.cancel_session(session_id)
362
+
363
+ if success:
364
+ return QRPairCancelResponse(
365
+ success=True,
366
+ message="Pairing session cancelled",
367
+ )
368
+ else:
369
+ return QRPairCancelResponse(
370
+ success=False,
371
+ message="Session not found or already completed",
372
+ )
@@ -0,0 +1,70 @@
1
+ """Workflow API 路由."""
2
+
3
+ from fastapi import APIRouter, HTTPException
4
+
5
+ from AutoGLM_GUI.schemas import (
6
+ WorkflowCreate,
7
+ WorkflowListResponse,
8
+ WorkflowResponse,
9
+ WorkflowUpdate,
10
+ )
11
+
12
+ router = APIRouter()
13
+
14
+
15
+ @router.get("/api/workflows", response_model=WorkflowListResponse)
16
+ def list_workflows() -> WorkflowListResponse:
17
+ """获取所有 workflows."""
18
+ from AutoGLM_GUI.workflow_manager import workflow_manager
19
+
20
+ workflows = workflow_manager.list_workflows()
21
+ return WorkflowListResponse(workflows=workflows)
22
+
23
+
24
+ @router.get("/api/workflows/{workflow_uuid}", response_model=WorkflowResponse)
25
+ def get_workflow(workflow_uuid: str) -> WorkflowResponse:
26
+ """获取单个 workflow."""
27
+ from AutoGLM_GUI.workflow_manager import workflow_manager
28
+
29
+ workflow = workflow_manager.get_workflow(workflow_uuid)
30
+ if not workflow:
31
+ raise HTTPException(status_code=404, detail="Workflow not found")
32
+ return WorkflowResponse(**workflow)
33
+
34
+
35
+ @router.post("/api/workflows", response_model=WorkflowResponse)
36
+ def create_workflow(request: WorkflowCreate) -> WorkflowResponse:
37
+ """创建新 workflow."""
38
+ from AutoGLM_GUI.workflow_manager import workflow_manager
39
+
40
+ try:
41
+ workflow = workflow_manager.create_workflow(
42
+ name=request.name, text=request.text
43
+ )
44
+ return WorkflowResponse(**workflow)
45
+ except Exception as e:
46
+ raise HTTPException(status_code=500, detail=str(e))
47
+
48
+
49
+ @router.put("/api/workflows/{workflow_uuid}", response_model=WorkflowResponse)
50
+ def update_workflow(workflow_uuid: str, request: WorkflowUpdate) -> WorkflowResponse:
51
+ """更新 workflow."""
52
+ from AutoGLM_GUI.workflow_manager import workflow_manager
53
+
54
+ workflow = workflow_manager.update_workflow(
55
+ uuid=workflow_uuid, name=request.name, text=request.text
56
+ )
57
+ if not workflow:
58
+ raise HTTPException(status_code=404, detail="Workflow not found")
59
+ return WorkflowResponse(**workflow)
60
+
61
+
62
+ @router.delete("/api/workflows/{workflow_uuid}")
63
+ def delete_workflow(workflow_uuid: str) -> dict:
64
+ """删除 workflow."""
65
+ from AutoGLM_GUI.workflow_manager import workflow_manager
66
+
67
+ success = workflow_manager.delete_workflow(workflow_uuid)
68
+ if not success:
69
+ raise HTTPException(status_code=404, detail="Workflow not found")
70
+ return {"success": True, "message": "Workflow deleted"}