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.
- AutoGLM_GUI/__init__.py +1 -1
- AutoGLM_GUI/__main__.py +11 -2
- AutoGLM_GUI/adb_plus/qr_pair.py +3 -3
- AutoGLM_GUI/agents/__init__.py +7 -2
- AutoGLM_GUI/agents/factory.py +46 -6
- AutoGLM_GUI/agents/glm/agent.py +2 -2
- AutoGLM_GUI/agents/glm/async_agent.py +515 -0
- AutoGLM_GUI/agents/glm/parser.py +4 -2
- AutoGLM_GUI/agents/protocols.py +111 -1
- AutoGLM_GUI/agents/stream_runner.py +4 -5
- AutoGLM_GUI/api/__init__.py +3 -1
- AutoGLM_GUI/api/agents.py +78 -37
- AutoGLM_GUI/api/devices.py +72 -0
- AutoGLM_GUI/api/layered_agent.py +9 -8
- AutoGLM_GUI/api/mcp.py +6 -4
- AutoGLM_GUI/config_manager.py +38 -1
- AutoGLM_GUI/device_manager.py +28 -4
- AutoGLM_GUI/device_metadata_manager.py +174 -0
- AutoGLM_GUI/devices/mock_device.py +8 -1
- AutoGLM_GUI/phone_agent_manager.py +145 -32
- AutoGLM_GUI/scheduler_manager.py +6 -6
- AutoGLM_GUI/schemas.py +89 -0
- AutoGLM_GUI/scrcpy_stream.py +2 -1
- AutoGLM_GUI/static/assets/{about-CfwX1Cmc.js → about-DTrVqEQH.js} +1 -1
- AutoGLM_GUI/static/assets/{alert-dialog-CtGlN2IJ.js → alert-dialog-B2KxPLtZ.js} +1 -1
- AutoGLM_GUI/static/assets/chat-BkrVbc3X.js +129 -0
- AutoGLM_GUI/static/assets/{circle-alert-t08bEMPO.js → circle-alert-vnNxOaxv.js} +1 -1
- AutoGLM_GUI/static/assets/{dialog-FNwZJFwk.js → dialog-Cuw3N8_F.js} +1 -1
- AutoGLM_GUI/static/assets/{eye-D0UPWCWC.js → eye-JD1jbm99.js} +1 -1
- AutoGLM_GUI/static/assets/{history-CRo95B7i.js → history-CobYdXju.js} +1 -1
- AutoGLM_GUI/static/assets/{index-CTHbFvKl.js → index-BzP-Te33.js} +5 -5
- AutoGLM_GUI/static/assets/index-y1vOOBHH.js +1 -0
- AutoGLM_GUI/static/assets/label-BpCMrXj_.js +1 -0
- AutoGLM_GUI/static/assets/{logs-RW09DyYY.js → logs-BcsSAeol.js} +1 -1
- AutoGLM_GUI/static/assets/{popover--JTJrE5v.js → popover-BHbCs5Wl.js} +1 -1
- AutoGLM_GUI/static/assets/scheduled-tasks-WvtmRsex.js +1 -0
- AutoGLM_GUI/static/assets/{textarea-PRmVnWq5.js → textarea-B84jf3cE.js} +1 -1
- AutoGLM_GUI/static/assets/workflows-DhBpqdz_.js +1 -0
- AutoGLM_GUI/static/index.html +1 -1
- {autoglm_gui-1.5.1.dist-info → autoglm_gui-1.5.3.dist-info}/METADATA +10 -1
- {autoglm_gui-1.5.1.dist-info → autoglm_gui-1.5.3.dist-info}/RECORD +44 -43
- AutoGLM_GUI/static/assets/chat-BYa-foUI.js +0 -129
- AutoGLM_GUI/static/assets/index-BaLMSqd3.js +0 -1
- AutoGLM_GUI/static/assets/label-DJFevVmr.js +0 -1
- AutoGLM_GUI/static/assets/scheduled-tasks-DTRKsQXF.js +0 -1
- AutoGLM_GUI/static/assets/square-pen-CPK_K680.js +0 -1
- AutoGLM_GUI/static/assets/workflows-CdcsAoaT.js +0 -1
- {autoglm_gui-1.5.1.dist-info → autoglm_gui-1.5.3.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.5.1.dist-info → autoglm_gui-1.5.3.dist-info}/entry_points.txt +0 -0
- {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[
|
|
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
|
-
|
|
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
|
-
) ->
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
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=
|
|
240
|
-
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
|
|
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
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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.
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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"
|
|
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:
|
AutoGLM_GUI/scheduler_manager.py
CHANGED
|
@@ -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
|
AutoGLM_GUI/scrcpy_stream.py
CHANGED
|
@@ -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-
|
|
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};
|