autoglm-gui 1.5.1__py3-none-any.whl → 1.5.3__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 (50) hide show
  1. AutoGLM_GUI/__init__.py +1 -1
  2. AutoGLM_GUI/__main__.py +11 -2
  3. AutoGLM_GUI/adb_plus/qr_pair.py +3 -3
  4. AutoGLM_GUI/agents/__init__.py +7 -2
  5. AutoGLM_GUI/agents/factory.py +46 -6
  6. AutoGLM_GUI/agents/glm/agent.py +2 -2
  7. AutoGLM_GUI/agents/glm/async_agent.py +515 -0
  8. AutoGLM_GUI/agents/glm/parser.py +4 -2
  9. AutoGLM_GUI/agents/protocols.py +111 -1
  10. AutoGLM_GUI/agents/stream_runner.py +4 -5
  11. AutoGLM_GUI/api/__init__.py +3 -1
  12. AutoGLM_GUI/api/agents.py +78 -37
  13. AutoGLM_GUI/api/devices.py +72 -0
  14. AutoGLM_GUI/api/layered_agent.py +9 -8
  15. AutoGLM_GUI/api/mcp.py +6 -4
  16. AutoGLM_GUI/config_manager.py +38 -1
  17. AutoGLM_GUI/device_manager.py +28 -4
  18. AutoGLM_GUI/device_metadata_manager.py +174 -0
  19. AutoGLM_GUI/devices/mock_device.py +8 -1
  20. AutoGLM_GUI/phone_agent_manager.py +145 -32
  21. AutoGLM_GUI/scheduler_manager.py +6 -6
  22. AutoGLM_GUI/schemas.py +89 -0
  23. AutoGLM_GUI/scrcpy_stream.py +2 -1
  24. AutoGLM_GUI/static/assets/{about-CfwX1Cmc.js → about-DTrVqEQH.js} +1 -1
  25. AutoGLM_GUI/static/assets/{alert-dialog-CtGlN2IJ.js → alert-dialog-B2KxPLtZ.js} +1 -1
  26. AutoGLM_GUI/static/assets/chat-BkrVbc3X.js +129 -0
  27. AutoGLM_GUI/static/assets/{circle-alert-t08bEMPO.js → circle-alert-vnNxOaxv.js} +1 -1
  28. AutoGLM_GUI/static/assets/{dialog-FNwZJFwk.js → dialog-Cuw3N8_F.js} +1 -1
  29. AutoGLM_GUI/static/assets/{eye-D0UPWCWC.js → eye-JD1jbm99.js} +1 -1
  30. AutoGLM_GUI/static/assets/{history-CRo95B7i.js → history-CobYdXju.js} +1 -1
  31. AutoGLM_GUI/static/assets/{index-CTHbFvKl.js → index-BzP-Te33.js} +5 -5
  32. AutoGLM_GUI/static/assets/index-y1vOOBHH.js +1 -0
  33. AutoGLM_GUI/static/assets/label-BpCMrXj_.js +1 -0
  34. AutoGLM_GUI/static/assets/{logs-RW09DyYY.js → logs-BcsSAeol.js} +1 -1
  35. AutoGLM_GUI/static/assets/{popover--JTJrE5v.js → popover-BHbCs5Wl.js} +1 -1
  36. AutoGLM_GUI/static/assets/scheduled-tasks-WvtmRsex.js +1 -0
  37. AutoGLM_GUI/static/assets/{textarea-PRmVnWq5.js → textarea-B84jf3cE.js} +1 -1
  38. AutoGLM_GUI/static/assets/workflows-DhBpqdz_.js +1 -0
  39. AutoGLM_GUI/static/index.html +1 -1
  40. {autoglm_gui-1.5.1.dist-info → autoglm_gui-1.5.3.dist-info}/METADATA +10 -1
  41. {autoglm_gui-1.5.1.dist-info → autoglm_gui-1.5.3.dist-info}/RECORD +44 -43
  42. AutoGLM_GUI/static/assets/chat-BYa-foUI.js +0 -129
  43. AutoGLM_GUI/static/assets/index-BaLMSqd3.js +0 -1
  44. AutoGLM_GUI/static/assets/label-DJFevVmr.js +0 -1
  45. AutoGLM_GUI/static/assets/scheduled-tasks-DTRKsQXF.js +0 -1
  46. AutoGLM_GUI/static/assets/square-pen-CPK_K680.js +0 -1
  47. AutoGLM_GUI/static/assets/workflows-CdcsAoaT.js +0 -1
  48. {autoglm_gui-1.5.1.dist-info → autoglm_gui-1.5.3.dist-info}/WHEEL +0 -0
  49. {autoglm_gui-1.5.1.dist-info → autoglm_gui-1.5.3.dist-info}/entry_points.txt +0 -0
  50. {autoglm_gui-1.5.1.dist-info → autoglm_gui-1.5.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,174 @@
1
+ """Device metadata persistence manager."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import threading
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ from AutoGLM_GUI.logger import logger
13
+
14
+ DISPLAY_NAME_MAX_LENGTH = 100
15
+
16
+
17
+ @dataclass
18
+ class DeviceMetadata:
19
+ """Device user-defined metadata."""
20
+
21
+ serial: str
22
+ display_name: Optional[str] = None
23
+ last_updated: datetime = field(default_factory=datetime.now)
24
+
25
+ def to_dict(self) -> dict:
26
+ """Convert to serializable dict."""
27
+ return {
28
+ "serial": self.serial,
29
+ "display_name": self.display_name,
30
+ "last_updated": self.last_updated.isoformat(),
31
+ }
32
+
33
+ @classmethod
34
+ def from_dict(cls, data: dict) -> "DeviceMetadata":
35
+ """Create instance from dict."""
36
+ last_updated_str = data.get("last_updated")
37
+ last_updated = (
38
+ datetime.fromisoformat(last_updated_str)
39
+ if last_updated_str
40
+ else datetime.now()
41
+ )
42
+ return cls(
43
+ serial=data.get("serial", ""),
44
+ display_name=data.get("display_name"),
45
+ last_updated=last_updated,
46
+ )
47
+
48
+
49
+ class DeviceMetadataManager:
50
+ """
51
+ Singleton manager for device metadata persistence.
52
+
53
+ Stores user-defined device names and other metadata.
54
+ Design: Lazy persistence - only save when metadata changes.
55
+ """
56
+
57
+ _instance: Optional[DeviceMetadataManager] = None
58
+ _lock = threading.Lock()
59
+
60
+ def __init__(self, storage_dir: Optional[Path] = None):
61
+ """Private constructor. Use get_instance() instead."""
62
+ if storage_dir is None:
63
+ storage_dir = Path.home() / ".config" / "autoglm" / "devices"
64
+
65
+ self.storage_dir = storage_dir
66
+ self.storage_dir.mkdir(parents=True, exist_ok=True)
67
+ self.metadata_file = self.storage_dir / "metadata.json"
68
+
69
+ self._metadata: dict[str, DeviceMetadata] = {}
70
+ self._data_lock = threading.RLock()
71
+
72
+ self._load_metadata()
73
+
74
+ @classmethod
75
+ def get_instance(cls, storage_dir: Optional[Path] = None) -> DeviceMetadataManager:
76
+ """Get singleton instance (thread-safe)."""
77
+ if cls._instance is None:
78
+ with cls._lock:
79
+ if cls._instance is None:
80
+ cls._instance = cls(storage_dir=storage_dir)
81
+ logger.info("DeviceMetadataManager singleton created")
82
+ return cls._instance
83
+
84
+ def _load_metadata(self) -> None:
85
+ """Load metadata from disk."""
86
+ if not self.metadata_file.exists():
87
+ logger.debug("No metadata file found, starting fresh")
88
+ return
89
+
90
+ try:
91
+ with open(self.metadata_file, encoding="utf-8") as f:
92
+ data = json.load(f)
93
+
94
+ with self._data_lock:
95
+ self._metadata = {
96
+ serial: DeviceMetadata.from_dict(meta_dict)
97
+ for serial, meta_dict in data.items()
98
+ }
99
+
100
+ logger.info(f"Loaded metadata for {len(self._metadata)} device(s)")
101
+ except Exception as e:
102
+ logger.error(f"Failed to load device metadata: {e}")
103
+ backup_path = self.metadata_file.with_suffix(".json.bak")
104
+ if self.metadata_file.exists():
105
+ try:
106
+ self.metadata_file.rename(backup_path)
107
+ logger.warning(
108
+ f"Corrupted metadata file moved to {backup_path.name}"
109
+ )
110
+ except Exception as backup_error:
111
+ logger.error(f"Failed to create backup: {backup_error}")
112
+ self._metadata = {}
113
+
114
+ def _save_metadata(self) -> None:
115
+ """Save metadata to disk atomically."""
116
+ temp_path = self.metadata_file.with_suffix(".json.tmp")
117
+ try:
118
+ with self._data_lock:
119
+ data = {
120
+ serial: meta.to_dict() for serial, meta in self._metadata.items()
121
+ }
122
+
123
+ with open(temp_path, "w", encoding="utf-8") as f:
124
+ json.dump(data, f, indent=2, ensure_ascii=False)
125
+
126
+ temp_path.replace(self.metadata_file)
127
+
128
+ logger.debug(f"Saved metadata for {len(self._metadata)} device(s)")
129
+ except Exception as e:
130
+ logger.error(f"Failed to save device metadata: {e}")
131
+ if temp_path.exists():
132
+ temp_path.unlink()
133
+ raise
134
+
135
+ def get_display_name(self, serial: str) -> Optional[str]:
136
+ """Get device display name by serial."""
137
+ with self._data_lock:
138
+ metadata = self._metadata.get(serial)
139
+ return metadata.display_name if metadata else None
140
+
141
+ def set_display_name(self, serial: str, display_name: Optional[str]) -> None:
142
+ """Set device display name. Empty string will be treated as None."""
143
+ normalized_name = display_name.strip() if display_name else None
144
+ normalized_name = normalized_name if normalized_name else None
145
+
146
+ if normalized_name and len(normalized_name) > DISPLAY_NAME_MAX_LENGTH:
147
+ raise ValueError(
148
+ f"Display name too long: {len(normalized_name)} > {DISPLAY_NAME_MAX_LENGTH}"
149
+ )
150
+
151
+ with self._data_lock:
152
+ if serial not in self._metadata:
153
+ self._metadata[serial] = DeviceMetadata(serial=serial)
154
+
155
+ current_name = self._metadata[serial].display_name
156
+ if current_name == normalized_name:
157
+ return
158
+
159
+ self._metadata[serial].display_name = normalized_name
160
+ self._metadata[serial].last_updated = datetime.now()
161
+
162
+ self._save_metadata()
163
+
164
+ logger.info(f"Updated display name for device {serial}: {normalized_name}")
165
+
166
+ def get_metadata(self, serial: str) -> Optional[DeviceMetadata]:
167
+ """Get full device metadata."""
168
+ with self._data_lock:
169
+ return self._metadata.get(serial)
170
+
171
+ def list_all_metadata(self) -> dict[str, DeviceMetadata]:
172
+ """List all stored device metadata."""
173
+ with self._data_lock:
174
+ return dict(self._metadata)
@@ -66,17 +66,24 @@ class MockDevice(DeviceProtocol):
66
66
 
67
67
  # === Input Operations ===
68
68
  def tap(self, x: int, y: int, delay: float | None = None) -> None:
69
- """Handle tap action through state machine."""
69
+ """Handle tap action through state machine.
70
+
71
+ Passes pixel coordinates directly to state machine (no conversion).
72
+ The click_region in scenario.yaml is in pixel coordinates.
73
+ """
74
+ # Pass pixel coordinates directly to state machine (no conversion)
70
75
  self._state_machine.handle_tap(x, y)
71
76
 
72
77
  def double_tap(self, x: int, y: int, delay: float | None = None) -> None:
73
78
  """Handle double tap (treated as single tap)."""
79
+ # Pass pixel coordinates directly to state machine (no conversion)
74
80
  self._state_machine.handle_tap(x, y)
75
81
 
76
82
  def long_press(
77
83
  self, x: int, y: int, duration_ms: int = 3000, delay: float | None = None
78
84
  ) -> None:
79
85
  """Handle long press (treated as tap for testing)."""
86
+ # Pass pixel coordinates directly to state machine (no conversion)
80
87
  self._state_machine.handle_tap(x, y)
81
88
 
82
89
  def swipe(
@@ -2,14 +2,15 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
5
6
  import threading
6
7
  import time
7
8
  from contextlib import contextmanager
8
9
  from dataclasses import dataclass
9
10
  from enum import Enum
10
- from typing import Callable, Optional
11
+ from typing import Awaitable, Callable, Optional
11
12
 
12
- from AutoGLM_GUI.agents.protocols import BaseAgent
13
+ from AutoGLM_GUI.agents.protocols import AsyncAgent, BaseAgent
13
14
  from AutoGLM_GUI.config import AgentConfig, ModelConfig
14
15
  from AutoGLM_GUI.exceptions import (
15
16
  AgentInitializationError,
@@ -96,10 +97,13 @@ class PhoneAgentManager:
96
97
  self._streaming_contexts: dict[str, StreamingAgentContext] = {}
97
98
  self._streaming_contexts_lock = threading.Lock()
98
99
 
99
- self._abort_events: dict[str, threading.Event | Callable[[], None]] = {}
100
+ self._abort_events: dict[
101
+ str, threading.Event | Callable[[], None] | Callable[[], Awaitable[None]]
102
+ ] = {}
100
103
 
101
104
  # Agent storage (transition from global state to instance state)
102
- self._agents: dict[str, BaseAgent] = {}
105
+ # Agents can be either AsyncAgent or BaseAgent depending on agent_type
106
+ self._agents: dict[str, AsyncAgent | BaseAgent] = {}
103
107
  self._agent_configs: dict[str, tuple[ModelConfig, AgentConfig]] = {}
104
108
 
105
109
  @classmethod
@@ -124,7 +128,7 @@ class PhoneAgentManager:
124
128
  takeover_callback: Optional[Callable] = None,
125
129
  confirmation_callback: Optional[Callable] = None,
126
130
  force: bool = False,
127
- ) -> "BaseAgent":
131
+ ) -> AsyncAgent | BaseAgent:
128
132
  from AutoGLM_GUI.agents import create_agent
129
133
 
130
134
  with self._manager_lock:
@@ -152,12 +156,19 @@ class PhoneAgentManager:
152
156
  from AutoGLM_GUI.device_manager import DeviceManager
153
157
 
154
158
  device_manager = DeviceManager.get_instance()
159
+ # Use agent_config.device_id (actual device ID) instead of device_id (storage key)
160
+ # to get device protocol, as device_id may be a composite key like "device_id:context"
161
+ actual_device_id = agent_config.device_id
162
+ if not actual_device_id:
163
+ raise AgentInitializationError(
164
+ "agent_config.device_id is required but was None"
165
+ )
155
166
  try:
156
- device = device_manager.get_device_protocol(device_id)
167
+ device = device_manager.get_device_protocol(actual_device_id)
157
168
  except ValueError:
158
169
  # Ensure cold starts refresh device cache before failing.
159
170
  device_manager.force_refresh()
160
- device = device_manager.get_device_protocol(device_id)
171
+ device = device_manager.get_device_protocol(actual_device_id)
161
172
 
162
173
  agent = create_agent(
163
174
  agent_type=agent_type,
@@ -190,14 +201,18 @@ class PhoneAgentManager:
190
201
  f"Failed to initialize agent: {str(e)}"
191
202
  ) from e
192
203
 
193
- def _auto_initialize_agent(self, device_id: str) -> None:
204
+ def _auto_initialize_agent(
205
+ self, agent_key: str, actual_device_id: str, agent_type: str | None = None
206
+ ) -> None:
194
207
  """
195
208
  使用全局配置自动初始化 agent(内部方法,需在 manager_lock 内调用).
196
209
 
197
210
  使用 factory 模式创建 agent,避免直接依赖 phone_agent.PhoneAgent。
198
211
 
199
212
  Args:
200
- device_id: 设备标识符
213
+ agent_key: Agent 存储键(可能是 device_id 或 device_id:context)
214
+ actual_device_id: 实际设备标识符(用于设备操作)
215
+ agent_type: 可选的 agent 类型覆盖
201
216
 
202
217
  Raises:
203
218
  AgentInitializationError: 如果配置不完整或初始化失败
@@ -208,7 +223,9 @@ class PhoneAgentManager:
208
223
  from AutoGLM_GUI.config_manager import config_manager
209
224
  from AutoGLM_GUI.types import AgentSpecificConfig
210
225
 
211
- logger.info(f"Auto-initializing agent for device {device_id}...")
226
+ logger.info(
227
+ f"Auto-initializing agent for key {agent_key} (device: {actual_device_id})..."
228
+ )
212
229
 
213
230
  # 热重载配置
214
231
  config_manager.load_file_config()
@@ -218,7 +235,7 @@ class PhoneAgentManager:
218
235
 
219
236
  if not effective_config.base_url:
220
237
  raise AgentInitializationError(
221
- f"Cannot auto-initialize agent for {device_id}: base_url not configured. "
238
+ f"Cannot auto-initialize agent for {agent_key}: base_url not configured. "
222
239
  f"Please configure base_url via /api/config or call /api/init explicitly."
223
240
  )
224
241
 
@@ -229,28 +246,54 @@ class PhoneAgentManager:
229
246
  model_name=effective_config.model_name,
230
247
  )
231
248
 
232
- agent_config = AgentConfig(device_id=device_id)
249
+ # 使用实际的 device_id 创建 AgentConfig
250
+ agent_config = AgentConfig(device_id=actual_device_id)
233
251
 
234
252
  # 调用 factory 方法创建 agent(避免直接依赖 phone_agent)
235
253
  agent_specific_config = cast(
236
254
  AgentSpecificConfig, effective_config.agent_config_params or {}
237
255
  )
256
+ # 使用提供的 agent_type 或从配置中获取
257
+ effective_agent_type = agent_type or effective_config.agent_type
238
258
  self.initialize_agent_with_factory(
239
- device_id=device_id,
240
- agent_type=effective_config.agent_type,
259
+ device_id=agent_key,
260
+ agent_type=effective_agent_type,
241
261
  model_config=model_config,
242
262
  agent_config=agent_config,
243
263
  agent_specific_config=agent_specific_config,
244
264
  )
245
- logger.info(f"Agent auto-initialized for device {device_id}")
265
+ logger.info(f"Agent auto-initialized for key {agent_key}")
266
+
267
+ def get_agent(self, device_id: str) -> AsyncAgent | BaseAgent:
268
+ """Get agent using default context (backward compatible)."""
269
+ return self.get_agent_with_context(device_id, context="default")
270
+
271
+ def get_agent_with_context(
272
+ self,
273
+ device_id: str,
274
+ context: str = "default",
275
+ agent_type: str | None = None,
276
+ ) -> AsyncAgent | BaseAgent:
277
+ """Get or create agent for specific context.
246
278
 
247
- def get_agent(self, device_id: str) -> BaseAgent:
279
+ Args:
280
+ device_id: Device identifier
281
+ context: Context identifier (e.g., "chat", "default")
282
+ agent_type: Optional agent type override
283
+
284
+ Returns:
285
+ Agent instance for this device+context combination
286
+ """
248
287
  with self._manager_lock:
249
- if device_id not in self._agents:
250
- self._auto_initialize_agent(device_id)
251
- return self._agents[device_id]
288
+ # Use composite key for context isolation (except for default)
289
+ agent_key = device_id if context == "default" else f"{device_id}:{context}"
252
290
 
253
- def get_agent_safe(self, device_id: str) -> Optional[BaseAgent]:
291
+ if agent_key not in self._agents:
292
+ self._auto_initialize_agent(agent_key, device_id, agent_type=agent_type)
293
+
294
+ return self._agents[agent_key]
295
+
296
+ def get_agent_safe(self, device_id: str) -> AsyncAgent | BaseAgent | None:
254
297
  with self._manager_lock:
255
298
  return self._agents.get(device_id)
256
299
 
@@ -363,7 +406,7 @@ class PhoneAgentManager:
363
406
  # Double-check locking pattern for thread safety
364
407
  with self._manager_lock:
365
408
  if not self.is_initialized(device_id):
366
- self._auto_initialize_agent(device_id)
409
+ self._auto_initialize_agent(device_id, device_id)
367
410
  else:
368
411
  raise AgentNotInitializedError(
369
412
  f"Agent not initialized for device {device_id}. "
@@ -512,18 +555,62 @@ class PhoneAgentManager:
512
555
  return self._metadata.get(device_id)
513
556
 
514
557
  def register_abort_handler(
515
- self, device_id: str, abort_handler: threading.Event | Callable[[], None]
558
+ self,
559
+ device_id: str,
560
+ abort_handler: threading.Event
561
+ | Callable[[], None]
562
+ | Callable[[], Awaitable[None]],
516
563
  ) -> None:
564
+ """注册取消处理器 (支持同步和异步处理器)。
565
+
566
+ Args:
567
+ device_id: 设备标识符
568
+ abort_handler: 取消处理器 (Event / 同步函数 / 异步函数)
569
+ """
517
570
  with self._streaming_contexts_lock:
518
571
  self._abort_events[device_id] = abort_handler
519
572
 
520
573
  def unregister_abort_handler(self, device_id: str) -> None:
574
+ """注销取消处理器。
575
+
576
+ Args:
577
+ device_id: 设备标识符
578
+ """
521
579
  with self._streaming_contexts_lock:
522
580
  self._abort_events.pop(device_id, None)
523
581
 
524
- def abort_streaming_chat(self, device_id: str) -> bool:
582
+ async def abort_streaming_chat_async(self, device_id: str) -> bool:
583
+ """异步中止流式对话 (支持 AsyncAgent)。
584
+
585
+ Args:
586
+ device_id: 设备标识符
587
+
588
+ Returns:
589
+ bool: True 表示发送了中止信号,False 表示没有活跃会话
525
590
  """
526
- 中止正在进行的流式对话.
591
+ with self._streaming_contexts_lock:
592
+ if device_id not in self._abort_events:
593
+ logger.warning(f"No active streaming chat for device {device_id}")
594
+ return False
595
+
596
+ logger.info(f"Aborting async streaming chat for device {device_id}")
597
+ handler = self._abort_events[device_id]
598
+
599
+ # 执行取消 (根据类型选择方式)
600
+ if isinstance(handler, threading.Event):
601
+ handler.set()
602
+ elif asyncio.iscoroutinefunction(handler):
603
+ await handler()
604
+ elif callable(handler):
605
+ handler()
606
+ else:
607
+ logger.warning(f"Unknown abort handler type: {type(handler)}")
608
+ return False
609
+
610
+ return True
611
+
612
+ def abort_streaming_chat(self, device_id: str) -> bool:
613
+ """同步中止流式对话 (向后兼容)。
527
614
 
528
615
  Args:
529
616
  device_id: 设备标识符
@@ -532,16 +619,42 @@ class PhoneAgentManager:
532
619
  bool: True 表示发送了中止信号,False 表示没有活跃会话
533
620
  """
534
621
  with self._streaming_contexts_lock:
535
- if device_id in self._abort_events:
536
- logger.info(f"Aborting streaming chat for device {device_id}")
537
- handler = self._abort_events[device_id]
538
- if isinstance(handler, threading.Event):
539
- handler.set()
540
- elif callable(handler):
541
- handler()
622
+ if device_id not in self._abort_events:
623
+ logger.warning(f"No active streaming chat for device {device_id}")
624
+ return False
625
+
626
+ logger.info(f"Aborting streaming chat for device {device_id}")
627
+ handler = self._abort_events[device_id]
628
+
629
+ if isinstance(handler, threading.Event):
630
+ handler.set()
631
+ return True
632
+ elif asyncio.iscoroutinefunction(handler):
633
+ logger.warning(
634
+ f"Detected async handler for {device_id}, "
635
+ f"but called sync abort. Use abort_streaming_chat_async instead."
636
+ )
637
+ # 尝试在当前线程的 event loop 中运行
638
+ try:
639
+ loop = asyncio.get_event_loop()
640
+ if loop.is_running():
641
+ # 不能在运行中的 loop 中调用 run_until_complete
642
+ # 创建一个 task
643
+ asyncio.create_task(self.abort_streaming_chat_async(device_id))
644
+ return True
645
+ else:
646
+ loop.run_until_complete(
647
+ self.abort_streaming_chat_async(device_id)
648
+ )
649
+ return True
650
+ except RuntimeError:
651
+ logger.error("Cannot abort async agent from sync context")
652
+ return False
653
+ elif callable(handler):
654
+ handler()
542
655
  return True
543
656
  else:
544
- logger.warning(f"No active streaming chat for device {device_id}")
657
+ logger.warning(f"Unknown abort handler type: {type(handler)}")
545
658
  return False
546
659
 
547
660
  def is_streaming_active(self, device_id: str) -> bool:
@@ -231,7 +231,7 @@ class SchedulerManager:
231
231
  task_success = False
232
232
 
233
233
  while agent.step_count < agent.agent_config.max_steps:
234
- step_result = agent.step(workflow["text"] if is_first else None)
234
+ step_result = agent.step(workflow["text"] if is_first else None) # type: ignore[misc]
235
235
  is_first = False
236
236
 
237
237
  # 收集每个 step 的消息
@@ -240,15 +240,15 @@ class SchedulerManager:
240
240
  role="assistant",
241
241
  content="",
242
242
  timestamp=datetime.now(),
243
- thinking=step_result.thinking,
244
- action=step_result.action,
243
+ thinking=step_result.thinking, # type: ignore[union-attr]
244
+ action=step_result.action, # type: ignore[union-attr]
245
245
  step=agent.step_count,
246
246
  )
247
247
  )
248
248
 
249
- if step_result.finished:
250
- result_message = step_result.message or "Task completed"
251
- task_success = step_result.success
249
+ if step_result.finished: # type: ignore[union-attr]
250
+ result_message = step_result.message or "Task completed" # type: ignore[union-attr]
251
+ task_success = step_result.success # type: ignore[union-attr]
252
252
  break
253
253
  else:
254
254
  result_message = "Max steps reached"
AutoGLM_GUI/schemas.py CHANGED
@@ -4,6 +4,8 @@ import re
4
4
 
5
5
  from pydantic import BaseModel, field_validator
6
6
 
7
+ from AutoGLM_GUI.device_metadata_manager import DISPLAY_NAME_MAX_LENGTH
8
+
7
9
 
8
10
  class InitRequest(BaseModel):
9
11
  device_id: str # Device ID (required)
@@ -274,6 +276,7 @@ class DeviceResponse(BaseModel):
274
276
  connection_type: str
275
277
  state: str
276
278
  is_available_only: bool
279
+ display_name: str | None = None
277
280
  agent: AgentStatusResponse | None = None
278
281
 
279
282
 
@@ -296,6 +299,9 @@ class ConfigResponse(BaseModel):
296
299
  # Agent 执行配置
297
300
  default_max_steps: int = 100 # 单次任务最大执行步数
298
301
 
302
+ # 分层代理配置
303
+ layered_max_turns: int = 50 # 分层代理模式的最大轮次
304
+
299
305
  # 决策模型配置(用于分层代理)
300
306
  decision_base_url: str | None = None
301
307
  decision_model_name: str | None = None
@@ -318,6 +324,9 @@ class ConfigSaveRequest(BaseModel):
318
324
  # Agent 执行配置
319
325
  default_max_steps: int | None = None # 单次任务最大执行步数
320
326
 
327
+ # 分层代理配置
328
+ layered_max_turns: int | None = None # 分层代理模式的最大轮次
329
+
321
330
  # 决策模型配置(用于分层代理)
322
331
  decision_base_url: str | None = None
323
332
  decision_model_name: str | None = None
@@ -335,6 +344,15 @@ class ConfigSaveRequest(BaseModel):
335
344
  raise ValueError("default_max_steps must be <= 1000")
336
345
  return v
337
346
 
347
+ @field_validator("layered_max_turns")
348
+ @classmethod
349
+ def validate_layered_max_turns(cls, v: int | None) -> int | None:
350
+ if v is None:
351
+ return v
352
+ if v < 1:
353
+ raise ValueError("layered_max_turns must be >= 1")
354
+ return v
355
+
338
356
  @field_validator("base_url")
339
357
  @classmethod
340
358
  def validate_base_url(cls, v: str) -> str:
@@ -816,3 +834,74 @@ class ScheduledTaskListResponse(BaseModel):
816
834
  """定时任务列表响应."""
817
835
 
818
836
  tasks: list[ScheduledTaskResponse]
837
+
838
+
839
+ class DeleteResponse(BaseModel):
840
+ success: bool
841
+ message: str
842
+
843
+
844
+ class ResetResponse(BaseModel):
845
+ success: bool
846
+ message: str
847
+ device_id: str
848
+
849
+
850
+ class ConfigSaveResponse(BaseModel):
851
+ success: bool
852
+ message: str
853
+ warnings: list[str] | None = None
854
+ destroyed_agents: int
855
+
856
+
857
+ class InitResponse(BaseModel):
858
+ success: bool
859
+ message: str
860
+ device_id: str
861
+ agent_type: str
862
+
863
+
864
+ class StreamResetResponse(BaseModel):
865
+ success: bool
866
+ message: str
867
+ device_id: str | None = None
868
+
869
+
870
+ class EnableDisableResponse(BaseModel):
871
+ success: bool
872
+ message: str
873
+ task_id: str
874
+ enabled: bool
875
+
876
+
877
+ # Device Name Models
878
+
879
+
880
+ class DeviceNameUpdateRequest(BaseModel):
881
+ """更新设备显示名称请求."""
882
+
883
+ display_name: str | None
884
+
885
+ @field_validator("display_name")
886
+ @classmethod
887
+ def validate_display_name(cls, v: str | None) -> str | None:
888
+ """验证 display_name."""
889
+ if v is None:
890
+ return v
891
+ v = v.strip()
892
+ if not v:
893
+ return None
894
+ if len(v) > DISPLAY_NAME_MAX_LENGTH:
895
+ raise ValueError(
896
+ f"display_name too long (max {DISPLAY_NAME_MAX_LENGTH} characters)"
897
+ )
898
+ return v
899
+
900
+
901
+ class DeviceNameResponse(BaseModel):
902
+ """设备显示名称响应."""
903
+
904
+ success: bool
905
+ serial: str
906
+ display_name: str | None = None
907
+ error: str | None = None
@@ -9,6 +9,7 @@ import time
9
9
  from dataclasses import dataclass
10
10
  from pathlib import Path
11
11
  from asyncio.subprocess import Process as AsyncProcess
12
+ from typing import AsyncGenerator
12
13
 
13
14
  from AutoGLM_GUI.adb_plus import check_device_available
14
15
  from AutoGLM_GUI.logger import logger
@@ -535,7 +536,7 @@ class ScrcpyStreamer:
535
536
  pts=pts,
536
537
  )
537
538
 
538
- async def iter_packets(self):
539
+ async def iter_packets(self) -> AsyncGenerator[ScrcpyMediaStreamPacket, None]:
539
540
  """Yield packets continuously from the scrcpy stream."""
540
541
  while True:
541
542
  yield await self.read_media_packet()
@@ -1 +1 @@
1
- import{j as o}from"./index-CTHbFvKl.js";function t(){return o.jsx("div",{className:"p-2",children:o.jsx("h3",{children:"About"})})}export{t as component};
1
+ import{j as o}from"./index-BzP-Te33.js";function t(){return o.jsx("div",{className:"p-2",children:o.jsx("h3",{children:"About"})})}export{t as component};