autoglm-gui 1.5.0__py3-none-any.whl → 1.5.2__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 +8 -3
- AutoGLM_GUI/agents/glm/async_agent.py +515 -0
- AutoGLM_GUI/agents/glm/parser.py +4 -2
- AutoGLM_GUI/agents/mai/agent.py +3 -0
- AutoGLM_GUI/agents/protocols.py +111 -1
- AutoGLM_GUI/agents/stream_runner.py +11 -7
- AutoGLM_GUI/api/__init__.py +3 -1
- AutoGLM_GUI/api/agents.py +103 -37
- AutoGLM_GUI/api/devices.py +72 -0
- AutoGLM_GUI/api/history.py +27 -1
- 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/models/history.py +45 -1
- AutoGLM_GUI/phone_agent_manager.py +145 -32
- AutoGLM_GUI/scheduler_manager.py +52 -6
- AutoGLM_GUI/schemas.py +101 -0
- AutoGLM_GUI/scrcpy_stream.py +2 -1
- AutoGLM_GUI/static/assets/{about-BQm96DAl.js → about-D7r9gCvG.js} +1 -1
- AutoGLM_GUI/static/assets/{alert-dialog-B42XxGPR.js → alert-dialog-BKM-yRiQ.js} +1 -1
- AutoGLM_GUI/static/assets/chat-k6TTD7PW.js +129 -0
- AutoGLM_GUI/static/assets/{circle-alert-D4rSJh37.js → circle-alert-sohSDLhl.js} +1 -1
- AutoGLM_GUI/static/assets/{dialog-DZ78cEcj.js → dialog-BgtPh0d5.js} +1 -1
- AutoGLM_GUI/static/assets/eye-DLqKbQmg.js +1 -0
- AutoGLM_GUI/static/assets/history-Bv1lfGUU.js +1 -0
- AutoGLM_GUI/static/assets/index-CV7jGxGm.css +1 -0
- AutoGLM_GUI/static/assets/index-CxWwh1VO.js +1 -0
- AutoGLM_GUI/static/assets/{index-CssG-3TH.js → index-SysdKciY.js} +5 -5
- AutoGLM_GUI/static/assets/label-DTUnzN4B.js +1 -0
- AutoGLM_GUI/static/assets/{logs-eoFxn5of.js → logs-BIhnDizW.js} +1 -1
- AutoGLM_GUI/static/assets/{popover-DLsuV5Sx.js → popover-CikYqu2P.js} +1 -1
- AutoGLM_GUI/static/assets/scheduled-tasks-B-KBsGbl.js +1 -0
- AutoGLM_GUI/static/assets/{textarea-BX6y7uM5.js → textarea-knJZrz77.js} +1 -1
- AutoGLM_GUI/static/assets/workflows-DzcSYwLZ.js +1 -0
- AutoGLM_GUI/static/index.html +2 -2
- {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/METADATA +58 -7
- autoglm_gui-1.5.2.dist-info/RECORD +119 -0
- AutoGLM_GUI/device_adapter.py +0 -263
- AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +0 -129
- AutoGLM_GUI/static/assets/history-DFBv7TGc.js +0 -1
- AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +0 -1
- AutoGLM_GUI/static/assets/index-CmZSnDqc.js +0 -1
- AutoGLM_GUI/static/assets/label-BCUzE_nm.js +0 -1
- AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +0 -1
- AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +0 -1
- AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +0 -1
- autoglm_gui-1.5.0.dist-info/RECORD +0 -157
- mai_agent/base.py +0 -137
- mai_agent/mai_grounding_agent.py +0 -263
- mai_agent/mai_naivigation_agent.py +0 -526
- mai_agent/prompt.py +0 -148
- mai_agent/unified_memory.py +0 -67
- mai_agent/utils.py +0 -73
- phone_agent/__init__.py +0 -12
- phone_agent/actions/__init__.py +0 -5
- phone_agent/actions/handler.py +0 -400
- phone_agent/actions/handler_ios.py +0 -278
- phone_agent/adb/__init__.py +0 -51
- phone_agent/adb/connection.py +0 -358
- phone_agent/adb/device.py +0 -253
- phone_agent/adb/input.py +0 -108
- phone_agent/adb/screenshot.py +0 -108
- phone_agent/agent.py +0 -253
- phone_agent/agent_ios.py +0 -277
- phone_agent/config/__init__.py +0 -53
- phone_agent/config/apps.py +0 -227
- phone_agent/config/apps_harmonyos.py +0 -256
- phone_agent/config/apps_ios.py +0 -339
- phone_agent/config/i18n.py +0 -81
- phone_agent/config/prompts.py +0 -80
- phone_agent/config/prompts_en.py +0 -79
- phone_agent/config/prompts_zh.py +0 -82
- phone_agent/config/timing.py +0 -167
- phone_agent/device_factory.py +0 -166
- phone_agent/hdc/__init__.py +0 -53
- phone_agent/hdc/connection.py +0 -384
- phone_agent/hdc/device.py +0 -269
- phone_agent/hdc/input.py +0 -145
- phone_agent/hdc/screenshot.py +0 -127
- phone_agent/model/__init__.py +0 -5
- phone_agent/model/client.py +0 -290
- phone_agent/xctest/__init__.py +0 -47
- phone_agent/xctest/connection.py +0 -379
- phone_agent/xctest/device.py +0 -472
- phone_agent/xctest/input.py +0 -311
- phone_agent/xctest/screenshot.py +0 -226
- {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/config_manager.py
CHANGED
|
@@ -23,6 +23,10 @@ from pydantic import BaseModel, field_validator
|
|
|
23
23
|
from AutoGLM_GUI.logger import logger
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
LAYERED_MAX_TURNS_DEFAULT = 50
|
|
27
|
+
LAYERED_MAX_TURNS_MIN = 1
|
|
28
|
+
|
|
29
|
+
|
|
26
30
|
# ==================== 配置源枚举 ====================
|
|
27
31
|
|
|
28
32
|
|
|
@@ -53,12 +57,14 @@ class ConfigModel(BaseModel):
|
|
|
53
57
|
api_key: str = "EMPTY"
|
|
54
58
|
|
|
55
59
|
# Agent 类型配置
|
|
56
|
-
agent_type: str = "glm" # Agent type (e.g., "glm", "mai")
|
|
60
|
+
agent_type: str = "glm" # Agent type (e.g., "glm", "mai", "glm-sync")
|
|
57
61
|
agent_config_params: dict | None = None # Agent-specific configuration
|
|
58
62
|
|
|
59
63
|
# Agent 执行配置
|
|
60
64
|
default_max_steps: int = 100 # 单次任务最大执行步数
|
|
61
65
|
|
|
66
|
+
layered_max_turns: int = LAYERED_MAX_TURNS_DEFAULT
|
|
67
|
+
|
|
62
68
|
# 决策模型配置(用于分层代理)
|
|
63
69
|
decision_base_url: str | None = None
|
|
64
70
|
decision_model_name: str | None = None
|
|
@@ -110,6 +116,13 @@ class ConfigModel(BaseModel):
|
|
|
110
116
|
raise ValueError("decision_model_name cannot be empty string")
|
|
111
117
|
return v.strip() if v else v
|
|
112
118
|
|
|
119
|
+
@field_validator("layered_max_turns")
|
|
120
|
+
@classmethod
|
|
121
|
+
def validate_layered_max_turns(cls, v: int) -> int:
|
|
122
|
+
if v < LAYERED_MAX_TURNS_MIN:
|
|
123
|
+
raise ValueError(f"layered_max_turns must be >= {LAYERED_MAX_TURNS_MIN}")
|
|
124
|
+
return v
|
|
125
|
+
|
|
113
126
|
|
|
114
127
|
# ==================== 配置层数据类 ====================
|
|
115
128
|
|
|
@@ -126,6 +139,7 @@ class ConfigLayer:
|
|
|
126
139
|
agent_config_params: Optional[dict] = None
|
|
127
140
|
# Agent 执行配置
|
|
128
141
|
default_max_steps: Optional[int] = None
|
|
142
|
+
layered_max_turns: Optional[int] = None
|
|
129
143
|
# 决策模型配置
|
|
130
144
|
decision_base_url: Optional[str] = None
|
|
131
145
|
decision_model_name: Optional[str] = None
|
|
@@ -160,6 +174,7 @@ class ConfigLayer:
|
|
|
160
174
|
"agent_type": self.agent_type,
|
|
161
175
|
"agent_config_params": self.agent_config_params,
|
|
162
176
|
"default_max_steps": self.default_max_steps,
|
|
177
|
+
"layered_max_turns": self.layered_max_turns,
|
|
163
178
|
"decision_base_url": self.decision_base_url,
|
|
164
179
|
"decision_model_name": self.decision_model_name,
|
|
165
180
|
"decision_api_key": self.decision_api_key,
|
|
@@ -225,6 +240,7 @@ class UnifiedConfigManager:
|
|
|
225
240
|
agent_type="glm",
|
|
226
241
|
agent_config_params=None,
|
|
227
242
|
default_max_steps=100,
|
|
243
|
+
layered_max_turns=LAYERED_MAX_TURNS_DEFAULT,
|
|
228
244
|
decision_base_url=None,
|
|
229
245
|
decision_model_name=None,
|
|
230
246
|
decision_api_key=None,
|
|
@@ -248,6 +264,7 @@ class UnifiedConfigManager:
|
|
|
248
264
|
base_url: Optional[str] = None,
|
|
249
265
|
model_name: Optional[str] = None,
|
|
250
266
|
api_key: Optional[str] = None,
|
|
267
|
+
layered_max_turns: Optional[int] = None,
|
|
251
268
|
) -> None:
|
|
252
269
|
"""
|
|
253
270
|
设置 CLI 参数配置(最高优先级).
|
|
@@ -256,11 +273,13 @@ class UnifiedConfigManager:
|
|
|
256
273
|
base_url: 从 --base-url 获取的值
|
|
257
274
|
model_name: 从 --model 获取的值
|
|
258
275
|
api_key: 从 --apikey 获取的值
|
|
276
|
+
layered_max_turns: 从 --layered-max-turns 获取的值
|
|
259
277
|
"""
|
|
260
278
|
self._cli_layer = ConfigLayer(
|
|
261
279
|
base_url=base_url,
|
|
262
280
|
model_name=model_name,
|
|
263
281
|
api_key=api_key,
|
|
282
|
+
layered_max_turns=layered_max_turns,
|
|
264
283
|
source=ConfigSource.CLI,
|
|
265
284
|
)
|
|
266
285
|
self._effective_config = None # 清除缓存
|
|
@@ -277,6 +296,7 @@ class UnifiedConfigManager:
|
|
|
277
296
|
- AUTOGLM_DECISION_BASE_URL
|
|
278
297
|
- AUTOGLM_DECISION_MODEL_NAME
|
|
279
298
|
- AUTOGLM_DECISION_API_KEY
|
|
299
|
+
- AUTOGLM_LAYERED_MAX_TURNS
|
|
280
300
|
"""
|
|
281
301
|
base_url = os.getenv("AUTOGLM_BASE_URL")
|
|
282
302
|
model_name = os.getenv("AUTOGLM_MODEL_NAME")
|
|
@@ -287,10 +307,19 @@ class UnifiedConfigManager:
|
|
|
287
307
|
decision_model_name = os.getenv("AUTOGLM_DECISION_MODEL_NAME")
|
|
288
308
|
decision_api_key = os.getenv("AUTOGLM_DECISION_API_KEY")
|
|
289
309
|
|
|
310
|
+
layered_max_turns_str = os.getenv("AUTOGLM_LAYERED_MAX_TURNS")
|
|
311
|
+
layered_max_turns = None
|
|
312
|
+
if layered_max_turns_str:
|
|
313
|
+
try:
|
|
314
|
+
layered_max_turns = int(layered_max_turns_str)
|
|
315
|
+
except ValueError:
|
|
316
|
+
logger.warning("AUTOGLM_LAYERED_MAX_TURNS must be an integer")
|
|
317
|
+
|
|
290
318
|
self._env_layer = ConfigLayer(
|
|
291
319
|
base_url=base_url if base_url else None,
|
|
292
320
|
model_name=model_name if model_name else None,
|
|
293
321
|
api_key=api_key if api_key else None,
|
|
322
|
+
layered_max_turns=layered_max_turns,
|
|
294
323
|
decision_base_url=decision_base_url if decision_base_url else None,
|
|
295
324
|
decision_model_name=decision_model_name if decision_model_name else None,
|
|
296
325
|
decision_api_key=decision_api_key if decision_api_key else None,
|
|
@@ -352,6 +381,7 @@ class UnifiedConfigManager:
|
|
|
352
381
|
), # 默认 'glm',兼容旧配置
|
|
353
382
|
agent_config_params=config_data.get("agent_config_params"),
|
|
354
383
|
default_max_steps=config_data.get("default_max_steps"),
|
|
384
|
+
layered_max_turns=config_data.get("layered_max_turns"),
|
|
355
385
|
decision_base_url=config_data.get("decision_base_url"),
|
|
356
386
|
decision_model_name=config_data.get("decision_model_name"),
|
|
357
387
|
decision_api_key=config_data.get("decision_api_key"),
|
|
@@ -385,6 +415,7 @@ class UnifiedConfigManager:
|
|
|
385
415
|
agent_type: Optional[str] = None,
|
|
386
416
|
agent_config_params: Optional[dict] = None,
|
|
387
417
|
default_max_steps: Optional[int] = None,
|
|
418
|
+
layered_max_turns: Optional[int] = None,
|
|
388
419
|
decision_base_url: Optional[str] = None,
|
|
389
420
|
decision_model_name: Optional[str] = None,
|
|
390
421
|
decision_api_key: Optional[str] = None,
|
|
@@ -400,6 +431,7 @@ class UnifiedConfigManager:
|
|
|
400
431
|
agent_type: Agent 类型(可选,如 "glm", "mai")
|
|
401
432
|
agent_config_params: Agent 特定配置参数(可选)
|
|
402
433
|
default_max_steps: 默认最大执行步数(可选)
|
|
434
|
+
layered_max_turns: 分层代理最大轮数(可选)
|
|
403
435
|
decision_base_url: 决策模型 Base URL(可选)
|
|
404
436
|
decision_model_name: 决策模型名称(可选)
|
|
405
437
|
decision_api_key: 决策模型 API Key(可选)
|
|
@@ -426,6 +458,8 @@ class UnifiedConfigManager:
|
|
|
426
458
|
new_config["agent_config_params"] = agent_config_params
|
|
427
459
|
if default_max_steps is not None:
|
|
428
460
|
new_config["default_max_steps"] = default_max_steps
|
|
461
|
+
if layered_max_turns is not None:
|
|
462
|
+
new_config["layered_max_turns"] = layered_max_turns
|
|
429
463
|
|
|
430
464
|
# 决策模型配置
|
|
431
465
|
if decision_base_url is not None:
|
|
@@ -447,6 +481,7 @@ class UnifiedConfigManager:
|
|
|
447
481
|
"agent_type",
|
|
448
482
|
"agent_config_params",
|
|
449
483
|
"default_max_steps",
|
|
484
|
+
"layered_max_turns",
|
|
450
485
|
"decision_base_url",
|
|
451
486
|
"decision_model_name",
|
|
452
487
|
"decision_api_key",
|
|
@@ -540,6 +575,7 @@ class UnifiedConfigManager:
|
|
|
540
575
|
"decision_base_url",
|
|
541
576
|
"decision_model_name",
|
|
542
577
|
"decision_api_key",
|
|
578
|
+
"layered_max_turns",
|
|
543
579
|
]
|
|
544
580
|
|
|
545
581
|
for key in config_keys:
|
|
@@ -708,6 +744,7 @@ class UnifiedConfigManager:
|
|
|
708
744
|
"decision_base_url": config.decision_base_url,
|
|
709
745
|
"decision_model_name": config.decision_model_name,
|
|
710
746
|
"decision_api_key": config.decision_api_key,
|
|
747
|
+
"layered_max_turns": config.layered_max_turns,
|
|
711
748
|
}
|
|
712
749
|
|
|
713
750
|
|
AutoGLM_GUI/device_manager.py
CHANGED
|
@@ -89,6 +89,7 @@ class ManagedDevice:
|
|
|
89
89
|
|
|
90
90
|
# Device metadata
|
|
91
91
|
model: Optional[str] = None
|
|
92
|
+
display_name: Optional[str] = None # User-defined custom name
|
|
92
93
|
|
|
93
94
|
# Device-level state
|
|
94
95
|
state: DeviceState = DeviceState.ONLINE
|
|
@@ -144,6 +145,7 @@ class ManagedDevice:
|
|
|
144
145
|
"id": self.primary_device_id,
|
|
145
146
|
"serial": self.serial,
|
|
146
147
|
"model": self.model or "Unknown",
|
|
148
|
+
"display_name": self.display_name,
|
|
147
149
|
"status": self.status,
|
|
148
150
|
"connection_type": self.connection_type.value,
|
|
149
151
|
"state": self.state.value,
|
|
@@ -178,24 +180,20 @@ def _create_managed_device(
|
|
|
178
180
|
for d in device_infos
|
|
179
181
|
]
|
|
180
182
|
|
|
181
|
-
# Extract model (prefer device with model info)
|
|
182
183
|
model = None
|
|
183
184
|
for device_info in device_infos:
|
|
184
185
|
if device_info.model:
|
|
185
186
|
model = device_info.model
|
|
186
187
|
break
|
|
187
188
|
|
|
188
|
-
# Create managed device
|
|
189
189
|
managed = ManagedDevice(
|
|
190
190
|
serial=serial,
|
|
191
191
|
connections=connections,
|
|
192
192
|
model=model,
|
|
193
193
|
)
|
|
194
194
|
|
|
195
|
-
# Select primary connection
|
|
196
195
|
managed.select_primary_connection()
|
|
197
196
|
|
|
198
|
-
# Set state
|
|
199
197
|
managed.state = (
|
|
200
198
|
DeviceState.ONLINE if managed.status == "device" else DeviceState.OFFLINE
|
|
201
199
|
)
|
|
@@ -249,6 +247,10 @@ class DeviceManager:
|
|
|
249
247
|
self._remote_devices: dict[str, "DeviceProtocol"] = {}
|
|
250
248
|
self._remote_device_configs: dict[str, dict] = {}
|
|
251
249
|
|
|
250
|
+
from AutoGLM_GUI.device_metadata_manager import DeviceMetadataManager
|
|
251
|
+
|
|
252
|
+
self._metadata_manager = DeviceMetadataManager.get_instance()
|
|
253
|
+
|
|
252
254
|
@classmethod
|
|
253
255
|
def get_instance(cls, adb_path: str = "adb") -> DeviceManager:
|
|
254
256
|
"""Get singleton instance (thread-safe)."""
|
|
@@ -449,6 +451,11 @@ class DeviceManager:
|
|
|
449
451
|
for serial in added_serials:
|
|
450
452
|
device_infos = grouped_by_serial[serial]
|
|
451
453
|
managed = _create_managed_device(serial, device_infos)
|
|
454
|
+
|
|
455
|
+
display_name = self._metadata_manager.get_display_name(serial)
|
|
456
|
+
if display_name:
|
|
457
|
+
managed.display_name = display_name
|
|
458
|
+
|
|
452
459
|
self._devices[serial] = managed
|
|
453
460
|
|
|
454
461
|
# Update reverse mapping
|
|
@@ -977,3 +984,20 @@ class DeviceManager:
|
|
|
977
984
|
from AutoGLM_GUI.devices.adb_device import ADBDevice
|
|
978
985
|
|
|
979
986
|
return ADBDevice(managed.primary_device_id)
|
|
987
|
+
|
|
988
|
+
def set_device_display_name(self, serial: str, display_name: Optional[str]) -> None:
|
|
989
|
+
"""Set custom display name for device."""
|
|
990
|
+
self._metadata_manager.set_display_name(serial, display_name)
|
|
991
|
+
|
|
992
|
+
with self._devices_lock:
|
|
993
|
+
if serial in self._devices:
|
|
994
|
+
self._devices[serial].display_name = display_name
|
|
995
|
+
logger.debug(f"Updated display name in memory for {serial}")
|
|
996
|
+
|
|
997
|
+
def get_device_display_name(self, serial: str) -> Optional[str]:
|
|
998
|
+
"""Get custom display name for device."""
|
|
999
|
+
with self._devices_lock:
|
|
1000
|
+
if serial in self._devices and self._devices[serial].display_name:
|
|
1001
|
+
return self._devices[serial].display_name
|
|
1002
|
+
|
|
1003
|
+
return self._metadata_manager.get_display_name(serial)
|
|
@@ -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(
|
AutoGLM_GUI/models/history.py
CHANGED
|
@@ -2,10 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from datetime import datetime
|
|
5
|
-
from typing import Literal
|
|
5
|
+
from typing import Any, Literal
|
|
6
6
|
from uuid import uuid4
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
@dataclass
|
|
10
|
+
class MessageRecord:
|
|
11
|
+
"""对话中的单条消息记录."""
|
|
12
|
+
|
|
13
|
+
role: Literal["user", "assistant"]
|
|
14
|
+
content: str
|
|
15
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
16
|
+
|
|
17
|
+
# assistant 消息特有字段
|
|
18
|
+
thinking: str | None = None
|
|
19
|
+
action: dict[str, Any] | None = None
|
|
20
|
+
step: int | None = None
|
|
21
|
+
|
|
22
|
+
def to_dict(self) -> dict:
|
|
23
|
+
"""转换为可序列化的字典."""
|
|
24
|
+
return {
|
|
25
|
+
"role": self.role,
|
|
26
|
+
"content": self.content,
|
|
27
|
+
"timestamp": self.timestamp.isoformat(),
|
|
28
|
+
"thinking": self.thinking,
|
|
29
|
+
"action": self.action,
|
|
30
|
+
"step": self.step,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_dict(cls, data: dict) -> "MessageRecord":
|
|
35
|
+
"""从字典创建实例."""
|
|
36
|
+
return cls(
|
|
37
|
+
role=data.get("role", "user"),
|
|
38
|
+
content=data.get("content", ""),
|
|
39
|
+
timestamp=datetime.fromisoformat(data["timestamp"])
|
|
40
|
+
if data.get("timestamp")
|
|
41
|
+
else datetime.now(),
|
|
42
|
+
thinking=data.get("thinking"),
|
|
43
|
+
action=data.get("action"),
|
|
44
|
+
step=data.get("step"),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
9
48
|
@dataclass
|
|
10
49
|
class ConversationRecord:
|
|
11
50
|
"""单条对话记录."""
|
|
@@ -30,6 +69,9 @@ class ConversationRecord:
|
|
|
30
69
|
# 错误信息
|
|
31
70
|
error_message: str | None = None
|
|
32
71
|
|
|
72
|
+
# 完整对话消息列表
|
|
73
|
+
messages: list[MessageRecord] = field(default_factory=list)
|
|
74
|
+
|
|
33
75
|
def to_dict(self) -> dict:
|
|
34
76
|
"""转换为可序列化的字典."""
|
|
35
77
|
return {
|
|
@@ -44,6 +86,7 @@ class ConversationRecord:
|
|
|
44
86
|
"source": self.source,
|
|
45
87
|
"source_detail": self.source_detail,
|
|
46
88
|
"error_message": self.error_message,
|
|
89
|
+
"messages": [m.to_dict() for m in self.messages],
|
|
47
90
|
}
|
|
48
91
|
|
|
49
92
|
@classmethod
|
|
@@ -65,6 +108,7 @@ class ConversationRecord:
|
|
|
65
108
|
source=data.get("source", "chat"),
|
|
66
109
|
source_detail=data.get("source_detail", ""),
|
|
67
110
|
error_message=data.get("error_message"),
|
|
111
|
+
messages=[MessageRecord.from_dict(m) for m in data.get("messages", [])],
|
|
68
112
|
)
|
|
69
113
|
|
|
70
114
|
|