autoglm-gui 1.5.5__py3-none-any.whl → 1.5.7__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 (39) hide show
  1. AutoGLM_GUI/api/agents.py +118 -93
  2. AutoGLM_GUI/api/devices.py +177 -0
  3. AutoGLM_GUI/api/scheduled_tasks.py +7 -2
  4. AutoGLM_GUI/device_group_manager.py +354 -0
  5. AutoGLM_GUI/models/__init__.py +8 -0
  6. AutoGLM_GUI/models/device_group.py +63 -0
  7. AutoGLM_GUI/models/scheduled_task.py +47 -4
  8. AutoGLM_GUI/scheduler_manager.py +255 -80
  9. AutoGLM_GUI/schemas.py +148 -3
  10. AutoGLM_GUI/static/assets/{about-BwLRPh96.js → about-BN7TF-uS.js} +1 -1
  11. AutoGLM_GUI/static/assets/{alert-dialog-BvDNaR9v.js → alert-dialog-D62FbnlZ.js} +1 -1
  12. AutoGLM_GUI/static/assets/chat-OpHm69J3.js +134 -0
  13. AutoGLM_GUI/static/assets/{dialog-IM0Ds7Lf.js → dialog-LOCyYNCR.js} +3 -3
  14. AutoGLM_GUI/static/assets/{eye-BWBwz8sy.js → eye-CphvL3Nj.js} +1 -1
  15. AutoGLM_GUI/static/assets/folder-open-1SAaazEb.js +1 -0
  16. AutoGLM_GUI/static/assets/{history-BkQlPjpV.js → history-Ef0dqgi8.js} +1 -1
  17. AutoGLM_GUI/static/assets/index-84TrNz7w.css +1 -0
  18. AutoGLM_GUI/static/assets/index-CGykihaE.js +1 -0
  19. AutoGLM_GUI/static/assets/index-DGbZjnSP.js +11 -0
  20. AutoGLM_GUI/static/assets/{label-CmQFo_IT.js → label-YliMUBOr.js} +1 -1
  21. AutoGLM_GUI/static/assets/logs-DeHKldwN.js +1 -0
  22. AutoGLM_GUI/static/assets/popover-DQ7GD1WC.js +1 -0
  23. AutoGLM_GUI/static/assets/scheduled-tasks-DL6pE9tE.js +1 -0
  24. AutoGLM_GUI/static/assets/{textarea-BlKvI11g.js → textarea-thgveQFy.js} +1 -1
  25. AutoGLM_GUI/static/assets/{workflows-CRq1fJf5.js → workflows-DRsmfMuf.js} +1 -1
  26. AutoGLM_GUI/static/index.html +2 -2
  27. {autoglm_gui-1.5.5.dist-info → autoglm_gui-1.5.7.dist-info}/METADATA +355 -6
  28. {autoglm_gui-1.5.5.dist-info → autoglm_gui-1.5.7.dist-info}/RECORD +31 -29
  29. AutoGLM_GUI/static/assets/chat-DAmrsouh.js +0 -129
  30. AutoGLM_GUI/static/assets/circle-alert-C8768IhH.js +0 -1
  31. AutoGLM_GUI/static/assets/index-B0fISVXF.js +0 -1
  32. AutoGLM_GUI/static/assets/index-CH4jPveL.js +0 -11
  33. AutoGLM_GUI/static/assets/index-DSIMVL8V.css +0 -1
  34. AutoGLM_GUI/static/assets/logs-ChcSA2r_.js +0 -1
  35. AutoGLM_GUI/static/assets/popover-BnpBfSOh.js +0 -1
  36. AutoGLM_GUI/static/assets/scheduled-tasks-BwgoPEdP.js +0 -1
  37. {autoglm_gui-1.5.5.dist-info → autoglm_gui-1.5.7.dist-info}/WHEEL +0 -0
  38. {autoglm_gui-1.5.5.dist-info → autoglm_gui-1.5.7.dist-info}/entry_points.txt +0 -0
  39. {autoglm_gui-1.5.5.dist-info → autoglm_gui-1.5.7.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/api/agents.py CHANGED
@@ -3,8 +3,8 @@
3
3
  import asyncio
4
4
  import json
5
5
 
6
- from fastapi import APIRouter, HTTPException
7
- from fastapi.responses import StreamingResponse
6
+ from fastapi import APIRouter, BackgroundTasks, HTTPException
7
+ from fastapi.responses import JSONResponse, StreamingResponse
8
8
  from pydantic import ValidationError
9
9
 
10
10
  from AutoGLM_GUI.config import AgentConfig, ModelConfig
@@ -203,7 +203,7 @@ async def chat(request: ChatRequest) -> ChatResponse:
203
203
 
204
204
 
205
205
  @router.post("/api/chat/stream")
206
- async def chat_stream(request: ChatRequest):
206
+ async def chat_stream(request: ChatRequest, background_tasks: BackgroundTasks):
207
207
  """发送任务给 Agent 并实时推送执行进度(SSE,多设备支持)。
208
208
 
209
209
  Agent 会在首次使用时自动初始化,无需手动调用 /api/init。
@@ -221,8 +221,53 @@ async def chat_stream(request: ChatRequest):
221
221
  device_id = request.device_id
222
222
  manager = PhoneAgentManager.get_instance()
223
223
 
224
+ # ===== 在外层获取设备锁 =====
225
+ acquired = False
226
+ try:
227
+ acquired = await asyncio.to_thread(
228
+ manager.acquire_device,
229
+ device_id,
230
+ timeout=0,
231
+ raise_on_timeout=True,
232
+ auto_initialize=True,
233
+ )
234
+ except DeviceBusyError:
235
+ logger.warning(f"Device {device_id} is busy, returning 409")
236
+ return JSONResponse(
237
+ status_code=409,
238
+ content={"detail": f"Device {device_id} is busy. Please wait."},
239
+ )
240
+ except AgentInitializationError as e:
241
+ logger.error(f"Failed to initialize agent for {device_id}: {e}")
242
+ return JSONResponse(
243
+ status_code=500,
244
+ content={
245
+ "detail": f"初始化失败: {str(e)}. 请检查全局配置 (base_url, api_key, model_name)"
246
+ },
247
+ )
248
+
249
+ logger.info(f"Device lock acquired for {device_id}")
250
+
251
+ # ===== 定义清理函数 =====
252
+ async def cleanup():
253
+ """Background task: 清理资源"""
254
+ try:
255
+ await asyncio.to_thread(manager.unregister_abort_handler, device_id)
256
+ logger.debug(f"Abort handler unregistered for {device_id}")
257
+ except Exception as e:
258
+ logger.warning(f"Failed to unregister abort handler for {device_id}: {e}")
259
+
260
+ if acquired:
261
+ try:
262
+ await asyncio.to_thread(manager.release_device, device_id)
263
+ logger.info(f"Device lock released for {device_id} (background task)")
264
+ except Exception as e:
265
+ logger.error(f"Failed to release device lock for {device_id}: {e}")
266
+
267
+ # ===== 注册 background task =====
268
+ background_tasks.add_task(cleanup)
269
+
224
270
  async def event_generator():
225
- acquired = False
226
271
  start_time = datetime.now()
227
272
  final_message = ""
228
273
  final_success = False
@@ -240,93 +285,57 @@ async def chat_stream(request: ChatRequest):
240
285
  )
241
286
 
242
287
  try:
243
- # 获取设备锁(在线程池中执行)
244
- acquired = await asyncio.to_thread(
245
- manager.acquire_device,
288
+ # 使用 chat context 获取 AsyncAgent
289
+ agent = await asyncio.to_thread(
290
+ manager.get_agent_with_context,
246
291
  device_id,
247
- timeout=0,
248
- raise_on_timeout=True,
249
- auto_initialize=True,
292
+ context="chat",
293
+ agent_type="glm-async",
250
294
  )
251
295
 
252
- try:
253
- # 使用 chat context 获取 AsyncAgent
254
- agent = await asyncio.to_thread(
255
- manager.get_agent_with_context,
256
- device_id,
257
- context="chat",
258
- agent_type="glm-async",
259
- )
260
-
261
- logger.info(f"Using AsyncAgent for device {device_id}")
262
-
263
- # 注册异步取消处理器
264
- async def cancel_handler():
265
- await agent.cancel() # type: ignore[union-attr]
266
-
267
- await asyncio.to_thread(
268
- manager.register_abort_handler, device_id, cancel_handler
269
- )
270
-
271
- try:
272
- # 直接使用 agent.stream()
273
- async for event in agent.stream(request.message): # type: ignore[union-attr]
274
- event_type = event["type"]
275
- event_data_dict = event["data"]
276
-
277
- # 收集每个 step 的消息
278
- if event_type == "step":
279
- messages.append(
280
- MessageRecord(
281
- role="assistant",
282
- content="",
283
- timestamp=datetime.now(),
284
- thinking=event_data_dict.get("thinking"),
285
- action=event_data_dict.get("action"),
286
- step=event_data_dict.get("step"),
287
- )
288
- )
289
-
290
- if event_type == "done":
291
- final_message = event_data_dict.get("message", "")
292
- final_success = event_data_dict.get("success", False)
293
- final_steps = event_data_dict.get("steps", 0)
294
-
295
- # 发送 SSE 事件
296
- sse_event = _create_sse_event(event_type, event_data_dict)
297
- yield f"event: {event_type}\n"
298
- yield f"data: {json.dumps(sse_event, ensure_ascii=False)}\n\n"
299
-
300
- except asyncio.CancelledError:
301
- logger.info(f"AsyncAgent task cancelled for device {device_id}")
302
- yield "event: cancelled\n"
303
- yield f"data: {json.dumps({'message': 'Task cancelled by user'})}\n\n"
304
- raise
305
-
306
- finally:
307
- await asyncio.to_thread(manager.unregister_abort_handler, device_id)
308
-
309
- finally:
310
- if acquired:
311
- await asyncio.to_thread(manager.release_device, device_id)
312
-
313
- device_manager = DeviceManager.get_instance()
314
- serialno = device_manager.get_serial_by_device_id(device_id)
315
- if serialno and final_message:
316
- end_time = datetime.now()
317
- record = ConversationRecord(
318
- task_text=request.message,
319
- final_message=final_message,
320
- success=final_success,
321
- steps=final_steps,
322
- start_time=start_time,
323
- end_time=end_time,
324
- duration_ms=int((end_time - start_time).total_seconds() * 1000),
325
- source="chat",
326
- error_message=None if final_success else final_message,
327
- messages=messages,
296
+ logger.info(f"Using AsyncAgent for device {device_id}")
297
+
298
+ # 注册异步取消处理器
299
+ async def cancel_handler():
300
+ await agent.cancel() # type: ignore[union-attr]
301
+
302
+ await asyncio.to_thread(
303
+ manager.register_abort_handler, device_id, cancel_handler
304
+ )
305
+
306
+ # 直接使用 agent.stream()
307
+ async for event in agent.stream(request.message): # type: ignore[union-attr]
308
+ event_type = event["type"]
309
+ event_data_dict = event["data"]
310
+
311
+ # 收集每个 step 的消息
312
+ if event_type == "step":
313
+ messages.append(
314
+ MessageRecord(
315
+ role="assistant",
316
+ content="",
317
+ timestamp=datetime.now(),
318
+ thinking=event_data_dict.get("thinking"),
319
+ action=event_data_dict.get("action"),
320
+ step=event_data_dict.get("step"),
321
+ )
328
322
  )
329
- history_manager.add_record(serialno, record)
323
+
324
+ if event_type == "done":
325
+ final_message = event_data_dict.get("message", "")
326
+ final_success = event_data_dict.get("success", False)
327
+ final_steps = event_data_dict.get("steps", 0)
328
+
329
+ # 发送 SSE 事件
330
+ sse_event = _create_sse_event(event_type, event_data_dict)
331
+ yield f"event: {event_type}\n"
332
+ yield f"data: {json.dumps(sse_event, ensure_ascii=False)}\n\n"
333
+
334
+ except asyncio.CancelledError:
335
+ logger.info(f"AsyncAgent task cancelled for device {device_id}")
336
+ yield "event: cancelled\n"
337
+ yield f"data: {json.dumps({'message': 'Task cancelled by user'})}\n\n"
338
+ # ✅ 不再 raise,让 generator 正常结束
330
339
 
331
340
  except AgentInitializationError as e:
332
341
  logger.error(f"Failed to initialize agent for {device_id}: {e}")
@@ -339,17 +348,33 @@ async def chat_stream(request: ChatRequest):
339
348
  )
340
349
  yield "event: error\n"
341
350
  yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"
342
- except DeviceBusyError:
343
- error_data = _create_sse_event("error", {"message": "Device is busy"})
344
- yield "event: error\n"
345
- yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"
351
+
346
352
  except Exception as e:
347
353
  logger.exception(f"Error in streaming chat for {device_id}")
348
354
  error_data = _create_sse_event("error", {"message": str(e)})
349
355
  yield "event: error\n"
350
356
  yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"
351
- finally:
352
- manager.unregister_abort_handler(device_id)
357
+
358
+ # ===== 保存历史记录 =====
359
+ device_manager = DeviceManager.get_instance()
360
+ serialno = device_manager.get_serial_by_device_id(device_id)
361
+ if serialno and final_message:
362
+ end_time = datetime.now()
363
+ record = ConversationRecord(
364
+ task_text=request.message,
365
+ final_message=final_message,
366
+ success=final_success,
367
+ steps=final_steps,
368
+ start_time=start_time,
369
+ end_time=end_time,
370
+ duration_ms=int((end_time - start_time).total_seconds() * 1000),
371
+ source="chat",
372
+ error_message=None if final_success else final_message,
373
+ messages=messages,
374
+ )
375
+ history_manager.add_record(serialno, record)
376
+
377
+ # Generator 正常结束,cleanup 会在 background task 中执行
353
378
 
354
379
  return StreamingResponse(
355
380
  event_generator(),
@@ -14,6 +14,13 @@ from AutoGLM_GUI.adb_plus.qr_pair import qr_pairing_manager
14
14
  from AutoGLM_GUI.logger import logger
15
15
 
16
16
  from AutoGLM_GUI.schemas import (
17
+ DeviceGroupAssignRequest,
18
+ DeviceGroupCreateRequest,
19
+ DeviceGroupListResponse,
20
+ DeviceGroupOperationResponse,
21
+ DeviceGroupReorderRequest,
22
+ DeviceGroupResponse,
23
+ DeviceGroupUpdateRequest,
17
24
  DeviceListResponse,
18
25
  DeviceNameResponse,
19
26
  DeviceNameUpdateRequest,
@@ -49,9 +56,13 @@ def _build_device_response_with_agent(
49
56
  API 层负责协调 DeviceManager 和 PhoneAgentManager,
50
57
  通过遍历设备的所有连接来查找已初始化的 Agent。
51
58
  """
59
+ from AutoGLM_GUI.device_group_manager import device_group_manager
52
60
 
53
61
  response = device.to_dict()
54
62
 
63
+ # 添加分组信息
64
+ response["group_id"] = device_group_manager.get_device_group(device.serial)
65
+
55
66
  # 遍历设备的所有连接,查找已初始化的 Agent
56
67
  # 使用 device.connections 公开属性(ManagedDevice 提供)
57
68
  for conn in device.connections:
@@ -526,3 +537,169 @@ def get_device_name(serial: str) -> DeviceNameResponse:
526
537
  serial=serial,
527
538
  error=f"Internal error: {str(e)}",
528
539
  )
540
+
541
+
542
+ # Device Group Routes
543
+
544
+
545
+ @router.get("/api/device-groups", response_model=DeviceGroupListResponse)
546
+ def list_device_groups() -> DeviceGroupListResponse:
547
+ """列出所有设备分组."""
548
+ from AutoGLM_GUI.device_group_manager import device_group_manager
549
+ from AutoGLM_GUI.device_manager import DeviceManager
550
+
551
+ groups = device_group_manager.list_groups()
552
+ device_manager = DeviceManager.get_instance()
553
+
554
+ # 获取当前所有设备的 serial 列表
555
+ managed_devices = device_manager.get_devices()
556
+ device_serials = {d.serial for d in managed_devices}
557
+
558
+ # 计算每个分组的设备数量
559
+ assignments = device_group_manager.get_all_assignments()
560
+
561
+ group_responses = []
562
+ for group in groups:
563
+ # 统计分配到该分组的设备数量(只计算当前在线/已知的设备)
564
+ if group.id == "default":
565
+ # 默认分组:包含未显式分配的设备
566
+ assigned_to_other = {
567
+ serial for serial, gid in assignments.items() if gid != "default"
568
+ }
569
+ device_count = len(device_serials - assigned_to_other)
570
+ else:
571
+ device_count = sum(
572
+ 1
573
+ for serial, gid in assignments.items()
574
+ if gid == group.id and serial in device_serials
575
+ )
576
+
577
+ group_responses.append(
578
+ DeviceGroupResponse(
579
+ id=group.id,
580
+ name=group.name,
581
+ order=group.order,
582
+ created_at=group.created_at.isoformat(),
583
+ updated_at=group.updated_at.isoformat(),
584
+ is_default=group.is_default,
585
+ device_count=device_count,
586
+ )
587
+ )
588
+
589
+ return DeviceGroupListResponse(groups=group_responses)
590
+
591
+
592
+ @router.post("/api/device-groups", response_model=DeviceGroupResponse)
593
+ def create_device_group(request: DeviceGroupCreateRequest) -> DeviceGroupResponse:
594
+ """创建新的设备分组."""
595
+ from AutoGLM_GUI.device_group_manager import device_group_manager
596
+
597
+ group = device_group_manager.create_group(request.name)
598
+
599
+ return DeviceGroupResponse(
600
+ id=group.id,
601
+ name=group.name,
602
+ order=group.order,
603
+ created_at=group.created_at.isoformat(),
604
+ updated_at=group.updated_at.isoformat(),
605
+ is_default=group.is_default,
606
+ device_count=0,
607
+ )
608
+
609
+
610
+ @router.put("/api/device-groups/{group_id}", response_model=DeviceGroupResponse)
611
+ def update_device_group(
612
+ group_id: str, request: DeviceGroupUpdateRequest
613
+ ) -> DeviceGroupResponse:
614
+ """更新设备分组名称."""
615
+ from fastapi import HTTPException
616
+
617
+ from AutoGLM_GUI.device_group_manager import device_group_manager
618
+
619
+ group = device_group_manager.update_group(group_id, request.name)
620
+ if not group:
621
+ raise HTTPException(status_code=404, detail="Group not found")
622
+
623
+ return DeviceGroupResponse(
624
+ id=group.id,
625
+ name=group.name,
626
+ order=group.order,
627
+ created_at=group.created_at.isoformat(),
628
+ updated_at=group.updated_at.isoformat(),
629
+ is_default=group.is_default,
630
+ device_count=0, # 不重新计算,前端可以刷新列表获取
631
+ )
632
+
633
+
634
+ @router.delete(
635
+ "/api/device-groups/{group_id}", response_model=DeviceGroupOperationResponse
636
+ )
637
+ def delete_device_group(group_id: str) -> DeviceGroupOperationResponse:
638
+ """删除设备分组(设备移回默认分组)."""
639
+ from AutoGLM_GUI.device_group_manager import device_group_manager
640
+ from AutoGLM_GUI.models.device_group import DEFAULT_GROUP_ID
641
+
642
+ if group_id == DEFAULT_GROUP_ID:
643
+ return DeviceGroupOperationResponse(
644
+ success=False,
645
+ message="Cannot delete default group",
646
+ error="cannot_delete_default",
647
+ )
648
+
649
+ success = device_group_manager.delete_group(group_id)
650
+
651
+ if success:
652
+ return DeviceGroupOperationResponse(
653
+ success=True,
654
+ message="Group deleted, devices moved to default group",
655
+ )
656
+ else:
657
+ return DeviceGroupOperationResponse(
658
+ success=False,
659
+ message="Group not found",
660
+ error="group_not_found",
661
+ )
662
+
663
+
664
+ @router.put("/api/device-groups/reorder", response_model=DeviceGroupOperationResponse)
665
+ def reorder_device_groups(
666
+ request: DeviceGroupReorderRequest,
667
+ ) -> DeviceGroupOperationResponse:
668
+ """调整设备分组顺序."""
669
+ from AutoGLM_GUI.device_group_manager import device_group_manager
670
+
671
+ success = device_group_manager.reorder_groups(request.group_ids)
672
+
673
+ if success:
674
+ return DeviceGroupOperationResponse(
675
+ success=True,
676
+ message="Groups reordered successfully",
677
+ )
678
+ else:
679
+ return DeviceGroupOperationResponse(
680
+ success=False,
681
+ message="Failed to reorder groups",
682
+ error="reorder_failed",
683
+ )
684
+
685
+
686
+ @router.put("/api/devices/{serial}/group", response_model=DeviceGroupOperationResponse)
687
+ def assign_device_to_group(
688
+ serial: str, request: DeviceGroupAssignRequest
689
+ ) -> DeviceGroupOperationResponse:
690
+ """分配设备到指定分组."""
691
+ from AutoGLM_GUI.device_group_manager import device_group_manager
692
+
693
+ success = device_group_manager.assign_device(serial, request.group_id)
694
+
695
+ if success:
696
+ return DeviceGroupOperationResponse(
697
+ success=True,
698
+ message=f"Device assigned to group {request.group_id}",
699
+ )
700
+ else:
701
+ return DeviceGroupOperationResponse(
702
+ success=False,
703
+ message="Failed to assign device to group",
704
+ error="assignment_failed",
705
+ )
@@ -19,13 +19,17 @@ def _task_to_response(task) -> ScheduledTaskResponse:
19
19
  id=task.id,
20
20
  name=task.name,
21
21
  workflow_uuid=task.workflow_uuid,
22
- device_serialno=task.device_serialno,
22
+ device_serialnos=task.device_serialnos,
23
+ device_group_id=task.device_group_id,
23
24
  cron_expression=task.cron_expression,
24
25
  enabled=task.enabled,
25
26
  created_at=task.created_at.isoformat(),
26
27
  updated_at=task.updated_at.isoformat(),
27
28
  last_run_time=task.last_run_time.isoformat() if task.last_run_time else None,
28
29
  last_run_success=task.last_run_success,
30
+ last_run_status=task.last_run_status,
31
+ last_run_success_count=task.last_run_success_count,
32
+ last_run_total_count=task.last_run_total_count,
29
33
  last_run_message=task.last_run_message,
30
34
  next_run_time=next_run.isoformat() if next_run else None,
31
35
  )
@@ -48,7 +52,8 @@ def create_scheduled_task(request: ScheduledTaskCreate) -> ScheduledTaskResponse
48
52
  task = scheduler_manager.create_task(
49
53
  name=request.name,
50
54
  workflow_uuid=request.workflow_uuid,
51
- device_serialno=request.device_serialno,
55
+ device_serialnos=request.device_serialnos,
56
+ device_group_id=request.device_group_id,
52
57
  cron_expression=request.cron_expression,
53
58
  enabled=request.enabled,
54
59
  )