autoglm-gui 1.4.1__py3-none-any.whl → 1.5.1__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 (135) hide show
  1. AutoGLM_GUI/__init__.py +11 -0
  2. AutoGLM_GUI/__main__.py +26 -4
  3. AutoGLM_GUI/actions/__init__.py +6 -0
  4. phone_agent/actions/handler_ios.py → AutoGLM_GUI/actions/handler.py +30 -112
  5. AutoGLM_GUI/actions/types.py +15 -0
  6. {phone_agent → AutoGLM_GUI}/adb/__init__.py +25 -23
  7. {phone_agent → AutoGLM_GUI}/adb/connection.py +5 -40
  8. {phone_agent → AutoGLM_GUI}/adb/device.py +12 -94
  9. {phone_agent → AutoGLM_GUI}/adb/input.py +6 -47
  10. AutoGLM_GUI/adb/screenshot.py +11 -0
  11. {phone_agent/config → AutoGLM_GUI/adb}/timing.py +1 -1
  12. AutoGLM_GUI/adb_plus/keyboard_installer.py +4 -2
  13. AutoGLM_GUI/adb_plus/screenshot.py +22 -1
  14. AutoGLM_GUI/adb_plus/serial.py +38 -20
  15. AutoGLM_GUI/adb_plus/touch.py +4 -9
  16. AutoGLM_GUI/agents/__init__.py +43 -12
  17. AutoGLM_GUI/agents/events.py +19 -0
  18. AutoGLM_GUI/agents/factory.py +31 -38
  19. AutoGLM_GUI/agents/glm/__init__.py +7 -0
  20. AutoGLM_GUI/agents/glm/agent.py +297 -0
  21. AutoGLM_GUI/agents/glm/message_builder.py +81 -0
  22. AutoGLM_GUI/agents/glm/parser.py +110 -0
  23. {phone_agent/config → AutoGLM_GUI/agents/glm}/prompts_en.py +7 -9
  24. {phone_agent/config → AutoGLM_GUI/agents/glm}/prompts_zh.py +18 -25
  25. AutoGLM_GUI/agents/mai/__init__.py +28 -0
  26. AutoGLM_GUI/agents/mai/agent.py +408 -0
  27. AutoGLM_GUI/agents/mai/parser.py +254 -0
  28. AutoGLM_GUI/agents/mai/prompts.py +103 -0
  29. AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
  30. AutoGLM_GUI/agents/protocols.py +12 -8
  31. AutoGLM_GUI/agents/stream_runner.py +193 -0
  32. AutoGLM_GUI/api/__init__.py +40 -21
  33. AutoGLM_GUI/api/agents.py +181 -239
  34. AutoGLM_GUI/api/control.py +9 -6
  35. AutoGLM_GUI/api/devices.py +102 -12
  36. AutoGLM_GUI/api/history.py +104 -0
  37. AutoGLM_GUI/api/layered_agent.py +67 -15
  38. AutoGLM_GUI/api/media.py +64 -1
  39. AutoGLM_GUI/api/scheduled_tasks.py +98 -0
  40. AutoGLM_GUI/config.py +81 -0
  41. AutoGLM_GUI/config_manager.py +68 -51
  42. AutoGLM_GUI/device_manager.py +248 -29
  43. AutoGLM_GUI/device_protocol.py +1 -1
  44. AutoGLM_GUI/devices/adb_device.py +5 -10
  45. AutoGLM_GUI/devices/mock_device.py +4 -2
  46. AutoGLM_GUI/devices/remote_device.py +8 -3
  47. AutoGLM_GUI/history_manager.py +164 -0
  48. AutoGLM_GUI/model/__init__.py +5 -0
  49. AutoGLM_GUI/model/message_builder.py +69 -0
  50. AutoGLM_GUI/model/types.py +24 -0
  51. AutoGLM_GUI/models/__init__.py +10 -0
  52. AutoGLM_GUI/models/history.py +140 -0
  53. AutoGLM_GUI/models/scheduled_task.py +71 -0
  54. AutoGLM_GUI/parsers/__init__.py +22 -0
  55. AutoGLM_GUI/parsers/base.py +50 -0
  56. AutoGLM_GUI/parsers/phone_parser.py +58 -0
  57. AutoGLM_GUI/phone_agent_manager.py +62 -396
  58. AutoGLM_GUI/platform_utils.py +26 -0
  59. AutoGLM_GUI/prompt_config.py +15 -0
  60. AutoGLM_GUI/prompts/__init__.py +32 -0
  61. AutoGLM_GUI/scheduler_manager.py +350 -0
  62. AutoGLM_GUI/schemas.py +246 -72
  63. AutoGLM_GUI/scrcpy_stream.py +142 -24
  64. AutoGLM_GUI/socketio_server.py +100 -27
  65. AutoGLM_GUI/static/assets/{about-_XNhzQZX.js → about-CfwX1Cmc.js} +1 -1
  66. AutoGLM_GUI/static/assets/alert-dialog-CtGlN2IJ.js +1 -0
  67. AutoGLM_GUI/static/assets/chat-BYa-foUI.js +129 -0
  68. AutoGLM_GUI/static/assets/circle-alert-t08bEMPO.js +1 -0
  69. AutoGLM_GUI/static/assets/dialog-FNwZJFwk.js +45 -0
  70. AutoGLM_GUI/static/assets/eye-D0UPWCWC.js +1 -0
  71. AutoGLM_GUI/static/assets/history-CRo95B7i.js +1 -0
  72. AutoGLM_GUI/static/assets/{index-Cy8TmmHV.js → index-BaLMSqd3.js} +1 -1
  73. AutoGLM_GUI/static/assets/index-CTHbFvKl.js +11 -0
  74. AutoGLM_GUI/static/assets/index-CV7jGxGm.css +1 -0
  75. AutoGLM_GUI/static/assets/label-DJFevVmr.js +1 -0
  76. AutoGLM_GUI/static/assets/logs-RW09DyYY.js +1 -0
  77. AutoGLM_GUI/static/assets/popover--JTJrE5v.js +1 -0
  78. AutoGLM_GUI/static/assets/scheduled-tasks-DTRKsQXF.js +1 -0
  79. AutoGLM_GUI/static/assets/square-pen-CPK_K680.js +1 -0
  80. AutoGLM_GUI/static/assets/textarea-PRmVnWq5.js +1 -0
  81. AutoGLM_GUI/static/assets/workflows-CdcsAoaT.js +1 -0
  82. AutoGLM_GUI/static/index.html +2 -2
  83. AutoGLM_GUI/types.py +17 -0
  84. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/METADATA +179 -130
  85. autoglm_gui-1.5.1.dist-info/RECORD +118 -0
  86. AutoGLM_GUI/agents/mai_adapter.py +0 -627
  87. AutoGLM_GUI/api/dual_model.py +0 -317
  88. AutoGLM_GUI/device_adapter.py +0 -263
  89. AutoGLM_GUI/dual_model/__init__.py +0 -53
  90. AutoGLM_GUI/dual_model/decision_model.py +0 -664
  91. AutoGLM_GUI/dual_model/dual_agent.py +0 -917
  92. AutoGLM_GUI/dual_model/protocols.py +0 -354
  93. AutoGLM_GUI/dual_model/vision_model.py +0 -442
  94. AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
  95. AutoGLM_GUI/phone_agent_patches.py +0 -147
  96. AutoGLM_GUI/static/assets/chat-DwJpiAWf.js +0 -126
  97. AutoGLM_GUI/static/assets/dialog-B3uW4T8V.js +0 -45
  98. AutoGLM_GUI/static/assets/index-Cpv2gSF1.css +0 -1
  99. AutoGLM_GUI/static/assets/index-UYYauTly.js +0 -12
  100. AutoGLM_GUI/static/assets/workflows-Du_de-dt.js +0 -1
  101. autoglm_gui-1.4.1.dist-info/RECORD +0 -117
  102. mai_agent/base.py +0 -137
  103. mai_agent/mai_grounding_agent.py +0 -263
  104. mai_agent/mai_naivigation_agent.py +0 -526
  105. mai_agent/prompt.py +0 -148
  106. mai_agent/unified_memory.py +0 -67
  107. mai_agent/utils.py +0 -73
  108. phone_agent/__init__.py +0 -12
  109. phone_agent/actions/__init__.py +0 -5
  110. phone_agent/actions/handler.py +0 -400
  111. phone_agent/adb/screenshot.py +0 -108
  112. phone_agent/agent.py +0 -253
  113. phone_agent/agent_ios.py +0 -277
  114. phone_agent/config/__init__.py +0 -53
  115. phone_agent/config/apps_harmonyos.py +0 -256
  116. phone_agent/config/apps_ios.py +0 -339
  117. phone_agent/config/prompts.py +0 -80
  118. phone_agent/device_factory.py +0 -166
  119. phone_agent/hdc/__init__.py +0 -53
  120. phone_agent/hdc/connection.py +0 -384
  121. phone_agent/hdc/device.py +0 -269
  122. phone_agent/hdc/input.py +0 -145
  123. phone_agent/hdc/screenshot.py +0 -127
  124. phone_agent/model/__init__.py +0 -5
  125. phone_agent/model/client.py +0 -290
  126. phone_agent/xctest/__init__.py +0 -47
  127. phone_agent/xctest/connection.py +0 -379
  128. phone_agent/xctest/device.py +0 -472
  129. phone_agent/xctest/input.py +0 -311
  130. phone_agent/xctest/screenshot.py +0 -226
  131. {phone_agent/config → AutoGLM_GUI/adb}/apps.py +0 -0
  132. {phone_agent/config → AutoGLM_GUI}/i18n.py +0 -0
  133. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/WHEEL +0 -0
  134. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/entry_points.txt +0 -0
  135. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -8,6 +8,8 @@ from typing import TYPE_CHECKING
8
8
 
9
9
  from AutoGLM_GUI.device_protocol import (
10
10
  DeviceInfo,
11
+ DeviceManagerProtocol,
12
+ DeviceProtocol,
11
13
  Screenshot,
12
14
  )
13
15
 
@@ -15,7 +17,7 @@ if TYPE_CHECKING:
15
17
  from tests.integration.state_machine import StateMachine
16
18
 
17
19
 
18
- class MockDevice:
20
+ class MockDevice(DeviceProtocol):
19
21
  """
20
22
  Mock device implementation driven by a state machine.
21
23
 
@@ -125,7 +127,7 @@ class MockDevice:
125
127
  pass
126
128
 
127
129
 
128
- class MockDeviceManager:
130
+ class MockDeviceManager(DeviceManagerProtocol):
129
131
  """
130
132
  Mock device manager for testing.
131
133
 
@@ -6,10 +6,15 @@ via HTTP, allowing remote control of devices.
6
6
 
7
7
  import httpx
8
8
 
9
- from AutoGLM_GUI.device_protocol import DeviceInfo, Screenshot
9
+ from AutoGLM_GUI.device_protocol import (
10
+ DeviceInfo,
11
+ DeviceManagerProtocol,
12
+ DeviceProtocol,
13
+ Screenshot,
14
+ )
10
15
 
11
16
 
12
- class RemoteDevice:
17
+ class RemoteDevice(DeviceProtocol):
13
18
  """
14
19
  Remote device implementation using HTTP.
15
20
 
@@ -126,7 +131,7 @@ class RemoteDevice:
126
131
  self.close()
127
132
 
128
133
 
129
- class RemoteDeviceManager:
134
+ class RemoteDeviceManager(DeviceManagerProtocol):
130
135
  """
131
136
  Remote device manager using HTTP.
132
137
 
@@ -0,0 +1,164 @@
1
+ """Conversation history manager with JSON file persistence."""
2
+
3
+ import hashlib
4
+ import json
5
+ import re
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ from AutoGLM_GUI.logger import logger
11
+ from AutoGLM_GUI.models.history import ConversationRecord, DeviceHistory
12
+
13
+ # ADB serialno 合法字符:字母数字、下划线、破折号、冒号、点
14
+ # USB: ABC123DEF456
15
+ # WiFi: 192.168.1.100:5555
16
+ # mDNS: adb-243a09b7._adb-tls-connect._tcp
17
+ _SERIALNO_PATTERN = re.compile(r"^[a-zA-Z0-9_\-:\.]+$")
18
+
19
+
20
+ class HistoryManager:
21
+ """对话历史管理器(单例模式)."""
22
+
23
+ _instance: Optional["HistoryManager"] = None
24
+
25
+ def __new__(cls):
26
+ if cls._instance is None:
27
+ cls._instance = super().__new__(cls)
28
+ return cls._instance
29
+
30
+ def __init__(self):
31
+ if hasattr(self, "_initialized"):
32
+ return
33
+ self._initialized = True
34
+ self._history_dir = Path.home() / ".config" / "autoglm" / "history"
35
+ self._file_cache: dict[str, DeviceHistory] = {}
36
+ self._file_mtime: dict[str, float] = {}
37
+
38
+ def _sanitize_serialno(self, serialno: str) -> str:
39
+ """将 serialno 转换为安全的文件名.
40
+
41
+ 如果 serialno 包含合法字符,直接使用;否则使用 SHA1 哈希作为文件名。
42
+ 这样可以防止路径遍历攻击,同时保证功能正常。
43
+ """
44
+ if not serialno:
45
+ return hashlib.sha1(b"empty").hexdigest()
46
+
47
+ # 检查是否包含路径遍历字符或不合法字符
48
+ if ".." in serialno or not _SERIALNO_PATTERN.match(serialno):
49
+ # 使用 SHA1 哈希作为安全的文件名
50
+ hashed = hashlib.sha1(serialno.encode("utf-8")).hexdigest()
51
+ logger.warning(
52
+ f"Unsafe serialno detected, using hash: {serialno!r} -> {hashed}"
53
+ )
54
+ return hashed
55
+
56
+ return serialno
57
+
58
+ def _get_history_path(self, serialno: str) -> Path:
59
+ """获取历史记录文件路径(带路径遍历防护)."""
60
+ safe_name = self._sanitize_serialno(serialno)
61
+ path = (self._history_dir / f"{safe_name}.json").resolve()
62
+
63
+ # 防御深度:确保解析后的路径仍在 history_dir 内
64
+ history_dir_resolved = self._history_dir.resolve()
65
+ if not path.is_relative_to(history_dir_resolved):
66
+ # 理论上不应该到这里,但作为最后防线
67
+ hashed = hashlib.sha1(serialno.encode("utf-8")).hexdigest()
68
+ logger.error(f"Path escape detected for {serialno!r}, using hash: {hashed}")
69
+ path = history_dir_resolved / f"{hashed}.json"
70
+
71
+ return path
72
+
73
+ def _load_history(self, serialno: str) -> DeviceHistory:
74
+ path = self._get_history_path(serialno)
75
+
76
+ if not path.exists():
77
+ return DeviceHistory(serialno=serialno)
78
+
79
+ current_mtime = path.stat().st_mtime
80
+ if (
81
+ serialno in self._file_mtime
82
+ and self._file_mtime[serialno] == current_mtime
83
+ and serialno in self._file_cache
84
+ ):
85
+ return self._file_cache[serialno]
86
+
87
+ try:
88
+ with open(path, encoding="utf-8") as f:
89
+ data = json.load(f)
90
+ history = DeviceHistory.from_dict(data)
91
+ self._file_cache[serialno] = history
92
+ self._file_mtime[serialno] = current_mtime
93
+ logger.debug(f"Loaded {len(history.records)} records for {serialno}")
94
+ return history
95
+ except (json.JSONDecodeError, FileNotFoundError) as e:
96
+ logger.warning(f"Failed to load history for {serialno}: {e}")
97
+ return DeviceHistory(serialno=serialno)
98
+
99
+ def _save_history(self, history: DeviceHistory) -> bool:
100
+ self._history_dir.mkdir(parents=True, exist_ok=True)
101
+ path = self._get_history_path(history.serialno)
102
+ temp_path = path.with_suffix(".tmp")
103
+
104
+ try:
105
+ history.last_updated = datetime.now()
106
+ with open(temp_path, "w", encoding="utf-8") as f:
107
+ json.dump(history.to_dict(), f, indent=2, ensure_ascii=False)
108
+ temp_path.replace(path)
109
+
110
+ self._file_cache[history.serialno] = history
111
+ self._file_mtime[history.serialno] = path.stat().st_mtime
112
+ logger.debug(f"Saved {len(history.records)} records for {history.serialno}")
113
+ return True
114
+ except Exception as e:
115
+ logger.error(f"Failed to save history for {history.serialno}: {e}")
116
+ if temp_path.exists():
117
+ temp_path.unlink()
118
+ return False
119
+
120
+ def add_record(self, serialno: str, record: ConversationRecord) -> None:
121
+ history = self._load_history(serialno)
122
+ history.records.insert(0, record)
123
+ self._save_history(history)
124
+ logger.info(f"Added history record for {serialno}: {record.id}")
125
+
126
+ def list_records(
127
+ self, serialno: str, limit: int = 50, offset: int = 0
128
+ ) -> list[ConversationRecord]:
129
+ history = self._load_history(serialno)
130
+ return history.records[offset : offset + limit]
131
+
132
+ def get_record(self, serialno: str, record_id: str) -> Optional[ConversationRecord]:
133
+ history = self._load_history(serialno)
134
+ return next((r for r in history.records if r.id == record_id), None)
135
+
136
+ def delete_record(self, serialno: str, record_id: str) -> bool:
137
+ history = self._load_history(serialno)
138
+ original_len = len(history.records)
139
+ history.records = [r for r in history.records if r.id != record_id]
140
+
141
+ if len(history.records) < original_len:
142
+ self._save_history(history)
143
+ logger.info(f"Deleted history record {record_id} for {serialno}")
144
+ return True
145
+
146
+ logger.warning(f"Record {record_id} not found for {serialno}")
147
+ return False
148
+
149
+ def clear_device_history(self, serialno: str) -> bool:
150
+ path = self._get_history_path(serialno)
151
+ if path.exists():
152
+ path.unlink()
153
+ self._file_cache.pop(serialno, None)
154
+ self._file_mtime.pop(serialno, None)
155
+ logger.info(f"Cleared all history for {serialno}")
156
+ return True
157
+ return False
158
+
159
+ def get_total_count(self, serialno: str) -> int:
160
+ history = self._load_history(serialno)
161
+ return len(history.records)
162
+
163
+
164
+ history_manager = HistoryManager()
@@ -0,0 +1,5 @@
1
+ """Model utilities for building messages."""
2
+
3
+ from .message_builder import MessageBuilder
4
+
5
+ __all__ = ["MessageBuilder"]
@@ -0,0 +1,69 @@
1
+ """Builder for constructing multimodal chat messages."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ class MessageBuilder:
7
+ @staticmethod
8
+ def create_system_message(content: str) -> dict[str, Any]:
9
+ return {"role": "system", "content": content}
10
+
11
+ @staticmethod
12
+ def create_user_message(
13
+ text: str, image_base64: str | None = None
14
+ ) -> dict[str, Any]:
15
+ if image_base64 is None:
16
+ return {"role": "user", "content": text}
17
+
18
+ return {
19
+ "role": "user",
20
+ "content": [
21
+ {"type": "text", "text": text},
22
+ {
23
+ "type": "image_url",
24
+ "image_url": {"url": f"data:image/png;base64,{image_base64}"},
25
+ },
26
+ ],
27
+ }
28
+
29
+ @staticmethod
30
+ def create_multi_image_user_message(
31
+ text: str, image_base64_list: list[str]
32
+ ) -> dict[str, Any]:
33
+ if not image_base64_list:
34
+ return {"role": "user", "content": text}
35
+
36
+ content_parts: list[dict[str, Any]] = [{"type": "text", "text": text}]
37
+
38
+ for image_base64 in image_base64_list:
39
+ content_parts.append(
40
+ {
41
+ "type": "image_url",
42
+ "image_url": {"url": f"data:image/png;base64,{image_base64}"},
43
+ }
44
+ )
45
+
46
+ return {"role": "user", "content": content_parts}
47
+
48
+ @staticmethod
49
+ def create_assistant_message(content: str) -> dict[str, Any]:
50
+ return {"role": "assistant", "content": content}
51
+
52
+ @staticmethod
53
+ def remove_images_from_message(message: dict[str, Any]) -> dict[str, Any]:
54
+ if message["role"] != "user":
55
+ return message
56
+
57
+ content = message["content"]
58
+ if isinstance(content, str):
59
+ return message
60
+
61
+ text_parts = [part for part in content if part["type"] == "text"]
62
+ if len(text_parts) == 1:
63
+ return {"role": "user", "content": text_parts[0]["text"]}
64
+
65
+ return {"role": "user", "content": text_parts}
66
+
67
+ @staticmethod
68
+ def build_screen_info(current_app: str) -> str:
69
+ return f"** Screen Info **\n\nCurrent App: {current_app}"
@@ -0,0 +1,24 @@
1
+ """Type definitions for model interactions."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class ModelResponse:
8
+ """Response from the vision-language model.
9
+
10
+ Attributes:
11
+ thinking: The model's reasoning process (from <think> tag)
12
+ action: The action to execute (from <answer> tag)
13
+ raw_content: Full response text from the model
14
+ time_to_first_token: Time until first token received (seconds)
15
+ time_to_thinking_end: Time until thinking phase completed (seconds)
16
+ total_time: Total inference time (seconds)
17
+ """
18
+
19
+ thinking: str
20
+ action: str
21
+ raw_content: str
22
+ time_to_first_token: float | None = None
23
+ time_to_thinking_end: float | None = None
24
+ total_time: float | None = None
@@ -0,0 +1,10 @@
1
+ """Data models for AutoGLM-GUI."""
2
+
3
+ from AutoGLM_GUI.models.history import ConversationRecord, DeviceHistory
4
+ from AutoGLM_GUI.models.scheduled_task import ScheduledTask
5
+
6
+ __all__ = [
7
+ "ConversationRecord",
8
+ "DeviceHistory",
9
+ "ScheduledTask",
10
+ ]
@@ -0,0 +1,140 @@
1
+ """Conversation history data models."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from typing import Any, Literal
6
+ from uuid import uuid4
7
+
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
+
48
+ @dataclass
49
+ class ConversationRecord:
50
+ """单条对话记录."""
51
+
52
+ id: str = field(default_factory=lambda: str(uuid4()))
53
+
54
+ # 任务信息
55
+ task_text: str = "" # 用户输入的任务
56
+ final_message: str = "" # 最终结果消息
57
+
58
+ # 执行信息
59
+ success: bool = False
60
+ steps: int = 0
61
+ start_time: datetime = field(default_factory=datetime.now)
62
+ end_time: datetime | None = None
63
+ duration_ms: int = 0 # 执行时长(毫秒)
64
+
65
+ # 来源标记
66
+ source: Literal["chat", "layered", "scheduled"] = "chat"
67
+ source_detail: str = "" # 定时任务名称 or session_id
68
+
69
+ # 错误信息
70
+ error_message: str | None = None
71
+
72
+ # 完整对话消息列表
73
+ messages: list[MessageRecord] = field(default_factory=list)
74
+
75
+ def to_dict(self) -> dict:
76
+ """转换为可序列化的字典."""
77
+ return {
78
+ "id": self.id,
79
+ "task_text": self.task_text,
80
+ "final_message": self.final_message,
81
+ "success": self.success,
82
+ "steps": self.steps,
83
+ "start_time": self.start_time.isoformat(),
84
+ "end_time": self.end_time.isoformat() if self.end_time else None,
85
+ "duration_ms": self.duration_ms,
86
+ "source": self.source,
87
+ "source_detail": self.source_detail,
88
+ "error_message": self.error_message,
89
+ "messages": [m.to_dict() for m in self.messages],
90
+ }
91
+
92
+ @classmethod
93
+ def from_dict(cls, data: dict) -> "ConversationRecord":
94
+ """从字典创建实例."""
95
+ return cls(
96
+ id=data.get("id", str(uuid4())),
97
+ task_text=data.get("task_text", ""),
98
+ final_message=data.get("final_message", ""),
99
+ success=data.get("success", False),
100
+ steps=data.get("steps", 0),
101
+ start_time=datetime.fromisoformat(data["start_time"])
102
+ if data.get("start_time")
103
+ else datetime.now(),
104
+ end_time=datetime.fromisoformat(data["end_time"])
105
+ if data.get("end_time")
106
+ else None,
107
+ duration_ms=data.get("duration_ms", 0),
108
+ source=data.get("source", "chat"),
109
+ source_detail=data.get("source_detail", ""),
110
+ error_message=data.get("error_message"),
111
+ messages=[MessageRecord.from_dict(m) for m in data.get("messages", [])],
112
+ )
113
+
114
+
115
+ @dataclass
116
+ class DeviceHistory:
117
+ """设备对话历史(一个设备一个文件)."""
118
+
119
+ serialno: str
120
+ records: list[ConversationRecord] = field(default_factory=list)
121
+ last_updated: datetime = field(default_factory=datetime.now)
122
+
123
+ def to_dict(self) -> dict:
124
+ """转换为可序列化的字典."""
125
+ return {
126
+ "serialno": self.serialno,
127
+ "records": [r.to_dict() for r in self.records],
128
+ "last_updated": self.last_updated.isoformat(),
129
+ }
130
+
131
+ @classmethod
132
+ def from_dict(cls, data: dict) -> "DeviceHistory":
133
+ """从字典创建实例."""
134
+ return cls(
135
+ serialno=data.get("serialno", ""),
136
+ records=[ConversationRecord.from_dict(r) for r in data.get("records", [])],
137
+ last_updated=datetime.fromisoformat(data["last_updated"])
138
+ if data.get("last_updated")
139
+ else datetime.now(),
140
+ )
@@ -0,0 +1,71 @@
1
+ """Scheduled task data models."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from uuid import uuid4
6
+
7
+
8
+ @dataclass
9
+ class ScheduledTask:
10
+ """定时任务定义."""
11
+
12
+ id: str = field(default_factory=lambda: str(uuid4()))
13
+
14
+ # 基础信息
15
+ name: str = "" # 任务名称
16
+ workflow_uuid: str = "" # 关联的 Workflow UUID
17
+ device_serialno: str = "" # 绑定的设备 serialno
18
+
19
+ # 调度配置
20
+ cron_expression: str = "" # Cron 表达式 (如 "0 8 * * *")
21
+ enabled: bool = True # 是否启用
22
+
23
+ # 元数据
24
+ created_at: datetime = field(default_factory=datetime.now)
25
+ updated_at: datetime = field(default_factory=datetime.now)
26
+
27
+ # 最近执行信息(只记录最后一次)
28
+ last_run_time: datetime | None = None
29
+ last_run_success: bool | None = None
30
+ last_run_message: str | None = None
31
+
32
+ def to_dict(self) -> dict:
33
+ """转换为可序列化的字典."""
34
+ return {
35
+ "id": self.id,
36
+ "name": self.name,
37
+ "workflow_uuid": self.workflow_uuid,
38
+ "device_serialno": self.device_serialno,
39
+ "cron_expression": self.cron_expression,
40
+ "enabled": self.enabled,
41
+ "created_at": self.created_at.isoformat(),
42
+ "updated_at": self.updated_at.isoformat(),
43
+ "last_run_time": self.last_run_time.isoformat()
44
+ if self.last_run_time
45
+ else None,
46
+ "last_run_success": self.last_run_success,
47
+ "last_run_message": self.last_run_message,
48
+ }
49
+
50
+ @classmethod
51
+ def from_dict(cls, data: dict) -> "ScheduledTask":
52
+ """从字典创建实例."""
53
+ return cls(
54
+ id=data.get("id", str(uuid4())),
55
+ name=data.get("name", ""),
56
+ workflow_uuid=data.get("workflow_uuid", ""),
57
+ device_serialno=data.get("device_serialno", ""),
58
+ cron_expression=data.get("cron_expression", ""),
59
+ enabled=data.get("enabled", True),
60
+ created_at=datetime.fromisoformat(data["created_at"])
61
+ if data.get("created_at")
62
+ else datetime.now(),
63
+ updated_at=datetime.fromisoformat(data["updated_at"])
64
+ if data.get("updated_at")
65
+ else datetime.now(),
66
+ last_run_time=datetime.fromisoformat(data["last_run_time"])
67
+ if data.get("last_run_time")
68
+ else None,
69
+ last_run_success=data.get("last_run_success"),
70
+ last_run_message=data.get("last_run_message"),
71
+ )
@@ -0,0 +1,22 @@
1
+ """Action parsers for different agent types.
2
+
3
+ This module provides parser implementations for converting model outputs
4
+ into standardized action dictionaries that can be executed by ActionHandler.
5
+
6
+ Each agent type has its own parser implementation:
7
+ - GLMParser: For GLM-based agents (enhanced AST parsing)
8
+ - PhoneAgentParser: For standard PhoneAgent (basic AST parsing)
9
+ - MAIParser: For MAI agent (XML + JSON parsing)
10
+ """
11
+
12
+ from .base import ActionParser
13
+ from AutoGLM_GUI.agents.glm.parser import GLMParser
14
+ from AutoGLM_GUI.agents.mai.parser import MAIParser
15
+ from .phone_parser import PhoneAgentParser
16
+
17
+ __all__ = [
18
+ "ActionParser",
19
+ "GLMParser",
20
+ "MAIParser",
21
+ "PhoneAgentParser",
22
+ ]
@@ -0,0 +1,50 @@
1
+ """Base protocol for action parsers.
2
+
3
+ This module defines the interface that all action parsers must implement.
4
+ """
5
+
6
+ from typing import Any, Protocol
7
+
8
+
9
+ class ActionParser(Protocol):
10
+ """Protocol for parsing model outputs into action dictionaries.
11
+
12
+ All parser implementations must provide:
13
+ 1. parse() method to convert raw model output into standardized action dict
14
+ 2. coordinate_scale property to specify the coordinate normalization range
15
+
16
+ The standardized action dictionary format:
17
+ {
18
+ "_metadata": "do" | "finish",
19
+ "action": "Tap" | "Swipe" | "Type" | ..., # Only when _metadata="do"
20
+ "coordinate": [x, y], # Normalized to 0-1000 range
21
+ "text": "...", # For Type action
22
+ ... # Other action-specific parameters
23
+ }
24
+ """
25
+
26
+ def parse(self, raw_response: str) -> dict[str, Any]:
27
+ """Parse raw model output into standardized action dictionary.
28
+
29
+ Args:
30
+ raw_response: Raw text output from the model.
31
+
32
+ Returns:
33
+ Standardized action dictionary with:
34
+ - "_metadata": "do" or "finish"
35
+ - "action": Action type (Tap, Swipe, etc.) when _metadata="do"
36
+ - Additional parameters based on action type
37
+
38
+ Raises:
39
+ ValueError: If the response cannot be parsed.
40
+ """
41
+ ...
42
+
43
+ @property
44
+ def coordinate_scale(self) -> int:
45
+ """Get the coordinate normalization scale used by this parser.
46
+
47
+ Returns:
48
+ 999 for MAI parser, 1000 for GLM/PhoneAgent parsers.
49
+ """
50
+ ...
@@ -0,0 +1,58 @@
1
+ """PhoneAgent parser using standard AST parsing."""
2
+
3
+ import ast
4
+ from typing import Any
5
+
6
+
7
+ class PhoneAgentParser:
8
+ """Parse PhoneAgent function-call style outputs using ast.parse.
9
+
10
+ Handles the same format as GLM but with simpler AST-based parsing.
11
+ Includes special handling for Type action to avoid parsing issues.
12
+ Coordinate scale: 0-1000
13
+ """
14
+
15
+ @property
16
+ def coordinate_scale(self) -> int:
17
+ return 1000
18
+
19
+ def parse(self, raw_response: str) -> dict[str, Any]:
20
+ response = raw_response.strip()
21
+
22
+ if response.startswith('do(action="Type"') or response.startswith(
23
+ 'do(action="Type_Name"'
24
+ ):
25
+ text = response.split("text=", 1)[1][1:-2]
26
+ action = {"_metadata": "do", "action": "Type", "text": text}
27
+ return action
28
+ elif response.startswith("do"):
29
+ try:
30
+ response = response.replace("\n", "\\n")
31
+ response = response.replace("\r", "\\r")
32
+ response = response.replace("\t", "\\t")
33
+
34
+ tree = ast.parse(response, mode="eval")
35
+ if not isinstance(tree.body, ast.Call):
36
+ raise ValueError("Expected a function call")
37
+
38
+ call = tree.body
39
+ action = {"_metadata": "do"}
40
+ for keyword in call.keywords:
41
+ key = keyword.arg
42
+ if key is None:
43
+ raise ValueError("Keyword argument name missing")
44
+ value = ast.literal_eval(keyword.value)
45
+ action[key] = value
46
+
47
+ return action
48
+ except (SyntaxError, ValueError) as e:
49
+ raise ValueError(f"Failed to parse do() action: {e}") from e
50
+
51
+ elif response.startswith("finish"):
52
+ action = {
53
+ "_metadata": "finish",
54
+ "message": response.replace("finish(message=", "")[1:-2],
55
+ }
56
+ return action
57
+ else:
58
+ raise ValueError(f"Failed to parse action: {response}")