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.
Files changed (97) 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 +8 -3
  7. AutoGLM_GUI/agents/glm/async_agent.py +515 -0
  8. AutoGLM_GUI/agents/glm/parser.py +4 -2
  9. AutoGLM_GUI/agents/mai/agent.py +3 -0
  10. AutoGLM_GUI/agents/protocols.py +111 -1
  11. AutoGLM_GUI/agents/stream_runner.py +11 -7
  12. AutoGLM_GUI/api/__init__.py +3 -1
  13. AutoGLM_GUI/api/agents.py +103 -37
  14. AutoGLM_GUI/api/devices.py +72 -0
  15. AutoGLM_GUI/api/history.py +27 -1
  16. AutoGLM_GUI/api/layered_agent.py +9 -8
  17. AutoGLM_GUI/api/mcp.py +6 -4
  18. AutoGLM_GUI/config_manager.py +38 -1
  19. AutoGLM_GUI/device_manager.py +28 -4
  20. AutoGLM_GUI/device_metadata_manager.py +174 -0
  21. AutoGLM_GUI/devices/mock_device.py +8 -1
  22. AutoGLM_GUI/models/history.py +45 -1
  23. AutoGLM_GUI/phone_agent_manager.py +145 -32
  24. AutoGLM_GUI/scheduler_manager.py +52 -6
  25. AutoGLM_GUI/schemas.py +101 -0
  26. AutoGLM_GUI/scrcpy_stream.py +2 -1
  27. AutoGLM_GUI/static/assets/{about-BQm96DAl.js → about-D7r9gCvG.js} +1 -1
  28. AutoGLM_GUI/static/assets/{alert-dialog-B42XxGPR.js → alert-dialog-BKM-yRiQ.js} +1 -1
  29. AutoGLM_GUI/static/assets/chat-k6TTD7PW.js +129 -0
  30. AutoGLM_GUI/static/assets/{circle-alert-D4rSJh37.js → circle-alert-sohSDLhl.js} +1 -1
  31. AutoGLM_GUI/static/assets/{dialog-DZ78cEcj.js → dialog-BgtPh0d5.js} +1 -1
  32. AutoGLM_GUI/static/assets/eye-DLqKbQmg.js +1 -0
  33. AutoGLM_GUI/static/assets/history-Bv1lfGUU.js +1 -0
  34. AutoGLM_GUI/static/assets/index-CV7jGxGm.css +1 -0
  35. AutoGLM_GUI/static/assets/index-CxWwh1VO.js +1 -0
  36. AutoGLM_GUI/static/assets/{index-CssG-3TH.js → index-SysdKciY.js} +5 -5
  37. AutoGLM_GUI/static/assets/label-DTUnzN4B.js +1 -0
  38. AutoGLM_GUI/static/assets/{logs-eoFxn5of.js → logs-BIhnDizW.js} +1 -1
  39. AutoGLM_GUI/static/assets/{popover-DLsuV5Sx.js → popover-CikYqu2P.js} +1 -1
  40. AutoGLM_GUI/static/assets/scheduled-tasks-B-KBsGbl.js +1 -0
  41. AutoGLM_GUI/static/assets/{textarea-BX6y7uM5.js → textarea-knJZrz77.js} +1 -1
  42. AutoGLM_GUI/static/assets/workflows-DzcSYwLZ.js +1 -0
  43. AutoGLM_GUI/static/index.html +2 -2
  44. {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/METADATA +58 -7
  45. autoglm_gui-1.5.2.dist-info/RECORD +119 -0
  46. AutoGLM_GUI/device_adapter.py +0 -263
  47. AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +0 -129
  48. AutoGLM_GUI/static/assets/history-DFBv7TGc.js +0 -1
  49. AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +0 -1
  50. AutoGLM_GUI/static/assets/index-CmZSnDqc.js +0 -1
  51. AutoGLM_GUI/static/assets/label-BCUzE_nm.js +0 -1
  52. AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +0 -1
  53. AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +0 -1
  54. AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +0 -1
  55. autoglm_gui-1.5.0.dist-info/RECORD +0 -157
  56. mai_agent/base.py +0 -137
  57. mai_agent/mai_grounding_agent.py +0 -263
  58. mai_agent/mai_naivigation_agent.py +0 -526
  59. mai_agent/prompt.py +0 -148
  60. mai_agent/unified_memory.py +0 -67
  61. mai_agent/utils.py +0 -73
  62. phone_agent/__init__.py +0 -12
  63. phone_agent/actions/__init__.py +0 -5
  64. phone_agent/actions/handler.py +0 -400
  65. phone_agent/actions/handler_ios.py +0 -278
  66. phone_agent/adb/__init__.py +0 -51
  67. phone_agent/adb/connection.py +0 -358
  68. phone_agent/adb/device.py +0 -253
  69. phone_agent/adb/input.py +0 -108
  70. phone_agent/adb/screenshot.py +0 -108
  71. phone_agent/agent.py +0 -253
  72. phone_agent/agent_ios.py +0 -277
  73. phone_agent/config/__init__.py +0 -53
  74. phone_agent/config/apps.py +0 -227
  75. phone_agent/config/apps_harmonyos.py +0 -256
  76. phone_agent/config/apps_ios.py +0 -339
  77. phone_agent/config/i18n.py +0 -81
  78. phone_agent/config/prompts.py +0 -80
  79. phone_agent/config/prompts_en.py +0 -79
  80. phone_agent/config/prompts_zh.py +0 -82
  81. phone_agent/config/timing.py +0 -167
  82. phone_agent/device_factory.py +0 -166
  83. phone_agent/hdc/__init__.py +0 -53
  84. phone_agent/hdc/connection.py +0 -384
  85. phone_agent/hdc/device.py +0 -269
  86. phone_agent/hdc/input.py +0 -145
  87. phone_agent/hdc/screenshot.py +0 -127
  88. phone_agent/model/__init__.py +0 -5
  89. phone_agent/model/client.py +0 -290
  90. phone_agent/xctest/__init__.py +0 -47
  91. phone_agent/xctest/connection.py +0 -379
  92. phone_agent/xctest/device.py +0 -472
  93. phone_agent/xctest/input.py +0 -311
  94. phone_agent/xctest/screenshot.py +0 -226
  95. {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/WHEEL +0 -0
  96. {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/entry_points.txt +0 -0
  97. {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/licenses/LICENSE +0 -0
@@ -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
 
@@ -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(
@@ -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