autoglm-gui 1.4.0__py3-none-any.whl → 1.5.0__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 (120) hide show
  1. AutoGLM_GUI/__init__.py +11 -0
  2. AutoGLM_GUI/__main__.py +26 -8
  3. AutoGLM_GUI/actions/__init__.py +6 -0
  4. AutoGLM_GUI/actions/handler.py +196 -0
  5. AutoGLM_GUI/actions/types.py +15 -0
  6. AutoGLM_GUI/adb/__init__.py +53 -0
  7. AutoGLM_GUI/adb/apps.py +227 -0
  8. AutoGLM_GUI/adb/connection.py +323 -0
  9. AutoGLM_GUI/adb/device.py +171 -0
  10. AutoGLM_GUI/adb/input.py +67 -0
  11. AutoGLM_GUI/adb/screenshot.py +11 -0
  12. AutoGLM_GUI/adb/timing.py +167 -0
  13. AutoGLM_GUI/adb_plus/keyboard_installer.py +4 -2
  14. AutoGLM_GUI/adb_plus/qr_pair.py +8 -8
  15. AutoGLM_GUI/adb_plus/screenshot.py +22 -1
  16. AutoGLM_GUI/adb_plus/serial.py +38 -20
  17. AutoGLM_GUI/adb_plus/touch.py +4 -9
  18. AutoGLM_GUI/agents/__init__.py +51 -0
  19. AutoGLM_GUI/agents/events.py +19 -0
  20. AutoGLM_GUI/agents/factory.py +153 -0
  21. AutoGLM_GUI/agents/glm/__init__.py +7 -0
  22. AutoGLM_GUI/agents/glm/agent.py +292 -0
  23. AutoGLM_GUI/agents/glm/message_builder.py +81 -0
  24. AutoGLM_GUI/agents/glm/parser.py +110 -0
  25. AutoGLM_GUI/agents/glm/prompts_en.py +77 -0
  26. AutoGLM_GUI/agents/glm/prompts_zh.py +75 -0
  27. AutoGLM_GUI/agents/mai/__init__.py +28 -0
  28. AutoGLM_GUI/agents/mai/agent.py +405 -0
  29. AutoGLM_GUI/agents/mai/parser.py +254 -0
  30. AutoGLM_GUI/agents/mai/prompts.py +103 -0
  31. AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
  32. AutoGLM_GUI/agents/protocols.py +27 -0
  33. AutoGLM_GUI/agents/stream_runner.py +188 -0
  34. AutoGLM_GUI/api/__init__.py +71 -11
  35. AutoGLM_GUI/api/agents.py +190 -229
  36. AutoGLM_GUI/api/control.py +9 -6
  37. AutoGLM_GUI/api/devices.py +112 -28
  38. AutoGLM_GUI/api/health.py +13 -0
  39. AutoGLM_GUI/api/history.py +78 -0
  40. AutoGLM_GUI/api/layered_agent.py +306 -181
  41. AutoGLM_GUI/api/mcp.py +11 -10
  42. AutoGLM_GUI/api/media.py +64 -1
  43. AutoGLM_GUI/api/scheduled_tasks.py +98 -0
  44. AutoGLM_GUI/api/version.py +23 -10
  45. AutoGLM_GUI/api/workflows.py +2 -1
  46. AutoGLM_GUI/config.py +72 -14
  47. AutoGLM_GUI/config_manager.py +98 -27
  48. AutoGLM_GUI/device_adapter.py +263 -0
  49. AutoGLM_GUI/device_manager.py +248 -29
  50. AutoGLM_GUI/device_protocol.py +266 -0
  51. AutoGLM_GUI/devices/__init__.py +49 -0
  52. AutoGLM_GUI/devices/adb_device.py +200 -0
  53. AutoGLM_GUI/devices/mock_device.py +185 -0
  54. AutoGLM_GUI/devices/remote_device.py +177 -0
  55. AutoGLM_GUI/exceptions.py +3 -3
  56. AutoGLM_GUI/history_manager.py +164 -0
  57. AutoGLM_GUI/i18n.py +81 -0
  58. AutoGLM_GUI/metrics.py +13 -20
  59. AutoGLM_GUI/model/__init__.py +5 -0
  60. AutoGLM_GUI/model/message_builder.py +69 -0
  61. AutoGLM_GUI/model/types.py +24 -0
  62. AutoGLM_GUI/models/__init__.py +10 -0
  63. AutoGLM_GUI/models/history.py +96 -0
  64. AutoGLM_GUI/models/scheduled_task.py +71 -0
  65. AutoGLM_GUI/parsers/__init__.py +22 -0
  66. AutoGLM_GUI/parsers/base.py +50 -0
  67. AutoGLM_GUI/parsers/phone_parser.py +58 -0
  68. AutoGLM_GUI/phone_agent_manager.py +118 -367
  69. AutoGLM_GUI/platform_utils.py +31 -2
  70. AutoGLM_GUI/prompt_config.py +15 -0
  71. AutoGLM_GUI/prompts/__init__.py +32 -0
  72. AutoGLM_GUI/scheduler_manager.py +304 -0
  73. AutoGLM_GUI/schemas.py +272 -63
  74. AutoGLM_GUI/scrcpy_stream.py +159 -37
  75. AutoGLM_GUI/server.py +3 -1
  76. AutoGLM_GUI/socketio_server.py +114 -29
  77. AutoGLM_GUI/state.py +10 -30
  78. AutoGLM_GUI/static/assets/{about-DeclntHg.js → about-BQm96DAl.js} +1 -1
  79. AutoGLM_GUI/static/assets/alert-dialog-B42XxGPR.js +1 -0
  80. AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +129 -0
  81. AutoGLM_GUI/static/assets/circle-alert-D4rSJh37.js +1 -0
  82. AutoGLM_GUI/static/assets/dialog-DZ78cEcj.js +45 -0
  83. AutoGLM_GUI/static/assets/history-DFBv7TGc.js +1 -0
  84. AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +1 -0
  85. AutoGLM_GUI/static/assets/{index-zQ4KKDHt.js → index-CmZSnDqc.js} +1 -1
  86. AutoGLM_GUI/static/assets/index-CssG-3TH.js +11 -0
  87. AutoGLM_GUI/static/assets/label-BCUzE_nm.js +1 -0
  88. AutoGLM_GUI/static/assets/logs-eoFxn5of.js +1 -0
  89. AutoGLM_GUI/static/assets/popover-DLsuV5Sx.js +1 -0
  90. AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +1 -0
  91. AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +1 -0
  92. AutoGLM_GUI/static/assets/textarea-BX6y7uM5.js +1 -0
  93. AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +1 -0
  94. AutoGLM_GUI/static/index.html +2 -2
  95. AutoGLM_GUI/types.py +142 -0
  96. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/METADATA +178 -92
  97. autoglm_gui-1.5.0.dist-info/RECORD +157 -0
  98. mai_agent/base.py +137 -0
  99. mai_agent/mai_grounding_agent.py +263 -0
  100. mai_agent/mai_naivigation_agent.py +526 -0
  101. mai_agent/prompt.py +148 -0
  102. mai_agent/unified_memory.py +67 -0
  103. mai_agent/utils.py +73 -0
  104. AutoGLM_GUI/api/dual_model.py +0 -311
  105. AutoGLM_GUI/dual_model/__init__.py +0 -53
  106. AutoGLM_GUI/dual_model/decision_model.py +0 -664
  107. AutoGLM_GUI/dual_model/dual_agent.py +0 -917
  108. AutoGLM_GUI/dual_model/protocols.py +0 -354
  109. AutoGLM_GUI/dual_model/vision_model.py +0 -442
  110. AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
  111. AutoGLM_GUI/phone_agent_patches.py +0 -146
  112. AutoGLM_GUI/static/assets/chat-Iut2yhSw.js +0 -125
  113. AutoGLM_GUI/static/assets/dialog-BfdcBs1x.js +0 -45
  114. AutoGLM_GUI/static/assets/index-5hCCwHA7.css +0 -1
  115. AutoGLM_GUI/static/assets/index-DHF1NZh0.js +0 -12
  116. AutoGLM_GUI/static/assets/workflows-xiplap-r.js +0 -1
  117. autoglm_gui-1.4.0.dist-info/RECORD +0 -100
  118. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/WHEEL +0 -0
  119. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/entry_points.txt +0 -0
  120. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,177 @@
1
+ """Remote Device implementation using HTTP.
2
+
3
+ This module provides a RemoteDevice that connects to a Device Agent
4
+ via HTTP, allowing remote control of devices.
5
+ """
6
+
7
+ import httpx
8
+
9
+ from AutoGLM_GUI.device_protocol import (
10
+ DeviceInfo,
11
+ DeviceManagerProtocol,
12
+ DeviceProtocol,
13
+ Screenshot,
14
+ )
15
+
16
+
17
+ class RemoteDevice(DeviceProtocol):
18
+ """
19
+ Remote device implementation using HTTP.
20
+
21
+ Connects to a Device Agent server that handles actual device operations.
22
+ The server decides the implementation (ADB, Accessibility, Mock, etc.).
23
+
24
+ Example:
25
+ >>> device = RemoteDevice("phone_001", "http://localhost:8001")
26
+ >>> screenshot = device.get_screenshot()
27
+ >>> device.tap(100, 200)
28
+ """
29
+
30
+ def __init__(self, device_id: str, base_url: str, timeout: float = 30.0):
31
+ self._device_id = device_id
32
+ self._base_url = base_url.rstrip("/")
33
+ self._client = httpx.Client(timeout=timeout)
34
+
35
+ @property
36
+ def device_id(self) -> str:
37
+ return self._device_id
38
+
39
+ def _post(self, endpoint: str, json: dict | None = None) -> dict:
40
+ """POST request helper."""
41
+ url = f"{self._base_url}/device/{self._device_id}{endpoint}"
42
+ resp = self._client.post(url, json=json or {})
43
+ resp.raise_for_status()
44
+ return resp.json()
45
+
46
+ def _get(self, endpoint: str) -> dict:
47
+ """GET request helper."""
48
+ url = f"{self._base_url}/device/{self._device_id}{endpoint}"
49
+ resp = self._client.get(url)
50
+ resp.raise_for_status()
51
+ return resp.json()
52
+
53
+ def get_screenshot(self, timeout: int = 10) -> Screenshot:
54
+ data = self._post("/screenshot", {"timeout": timeout})
55
+ return Screenshot(
56
+ base64_data=data["base64_data"],
57
+ width=data["width"],
58
+ height=data["height"],
59
+ is_sensitive=data.get("is_sensitive", False),
60
+ )
61
+
62
+ def tap(self, x: int, y: int, delay: float | None = None) -> None:
63
+ self._post("/tap", {"x": x, "y": y, "delay": delay})
64
+
65
+ def double_tap(self, x: int, y: int, delay: float | None = None) -> None:
66
+ self._post("/double_tap", {"x": x, "y": y, "delay": delay})
67
+
68
+ def long_press(
69
+ self, x: int, y: int, duration_ms: int = 3000, delay: float | None = None
70
+ ) -> None:
71
+ self._post(
72
+ "/long_press", {"x": x, "y": y, "duration_ms": duration_ms, "delay": delay}
73
+ )
74
+
75
+ def swipe(
76
+ self,
77
+ start_x: int,
78
+ start_y: int,
79
+ end_x: int,
80
+ end_y: int,
81
+ duration_ms: int | None = None,
82
+ delay: float | None = None,
83
+ ) -> None:
84
+ self._post(
85
+ "/swipe",
86
+ {
87
+ "start_x": start_x,
88
+ "start_y": start_y,
89
+ "end_x": end_x,
90
+ "end_y": end_y,
91
+ "duration_ms": duration_ms,
92
+ "delay": delay,
93
+ },
94
+ )
95
+
96
+ def type_text(self, text: str) -> None:
97
+ self._post("/type_text", {"text": text})
98
+
99
+ def clear_text(self) -> None:
100
+ self._post("/clear_text")
101
+
102
+ def back(self, delay: float | None = None) -> None:
103
+ self._post("/back", {"delay": delay})
104
+
105
+ def home(self, delay: float | None = None) -> None:
106
+ self._post("/home", {"delay": delay})
107
+
108
+ def launch_app(self, app_name: str, delay: float | None = None) -> bool:
109
+ data = self._post("/launch_app", {"app_name": app_name, "delay": delay})
110
+ return data.get("success", True)
111
+
112
+ def get_current_app(self) -> str:
113
+ data = self._get("/current_app")
114
+ return data["app_name"]
115
+
116
+ def detect_and_set_adb_keyboard(self) -> str:
117
+ data = self._post("/detect_keyboard")
118
+ return data.get("original_ime", "")
119
+
120
+ def restore_keyboard(self, ime: str) -> None:
121
+ self._post("/restore_keyboard", {"ime": ime})
122
+
123
+ def close(self) -> None:
124
+ """Close the HTTP client."""
125
+ self._client.close()
126
+
127
+ def __enter__(self):
128
+ return self
129
+
130
+ def __exit__(self, exc_type, exc_val, exc_tb):
131
+ self.close()
132
+
133
+
134
+ class RemoteDeviceManager(DeviceManagerProtocol):
135
+ """
136
+ Remote device manager using HTTP.
137
+
138
+ Manages connections to a Device Agent server.
139
+ """
140
+
141
+ def __init__(self, base_url: str, timeout: float = 30.0):
142
+ self._base_url = base_url.rstrip("/")
143
+ self._timeout = timeout
144
+ self._client = httpx.Client(timeout=timeout)
145
+ self._devices: dict[str, RemoteDevice] = {}
146
+
147
+ def list_devices(self) -> list[DeviceInfo]:
148
+ resp = self._client.get(f"{self._base_url}/devices")
149
+ resp.raise_for_status()
150
+ return [DeviceInfo(**d) for d in resp.json()]
151
+
152
+ def get_device(self, device_id: str) -> RemoteDevice:
153
+ if device_id not in self._devices:
154
+ self._devices[device_id] = RemoteDevice(
155
+ device_id, self._base_url, self._timeout
156
+ )
157
+ return self._devices[device_id]
158
+
159
+ def connect(self, address: str, timeout: int = 10) -> tuple[bool, str]:
160
+ resp = self._client.post(
161
+ f"{self._base_url}/connect", json={"address": address, "timeout": timeout}
162
+ )
163
+ data = resp.json()
164
+ return data.get("success", False), data.get("message", "")
165
+
166
+ def disconnect(self, device_id: str) -> tuple[bool, str]:
167
+ self._devices.pop(device_id, None)
168
+ resp = self._client.post(
169
+ f"{self._base_url}/disconnect", json={"device_id": device_id}
170
+ )
171
+ data = resp.json()
172
+ return data.get("success", True), data.get("message", "Disconnected")
173
+
174
+ def close(self) -> None:
175
+ for device in self._devices.values():
176
+ device.close()
177
+ self._client.close()
AutoGLM_GUI/exceptions.py CHANGED
@@ -79,9 +79,9 @@ class AgentInitializationError(Exception):
79
79
  How to fix:
80
80
  1. Check configuration:
81
81
  >>> from AutoGLM_GUI.config_manager import config_manager
82
- >>> config = config_manager.get_effective_config()
83
- >>> print(f"base_url: {config.base_url}")
84
- >>> print(f"model_name: {config.model_name}")
82
+ >>> effective_config = config_manager.get_effective_config()
83
+ >>> print(f"base_url: {effective_config.base_url}")
84
+ >>> print(f"model_name: {effective_config.model_name}")
85
85
 
86
86
  2. Set configuration:
87
87
  >>> via API: POST /api/config {"base_url": "...", "model_name": "...", "api_key": "..."}
@@ -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()
AutoGLM_GUI/i18n.py ADDED
@@ -0,0 +1,81 @@
1
+ """Internationalization (i18n) module for Phone Agent UI messages."""
2
+
3
+ # Chinese messages
4
+ MESSAGES_ZH = {
5
+ "thinking": "思考过程",
6
+ "action": "执行动作",
7
+ "task_completed": "任务完成",
8
+ "done": "完成",
9
+ "starting_task": "开始执行任务",
10
+ "final_result": "最终结果",
11
+ "task_result": "任务结果",
12
+ "confirmation_required": "需要确认",
13
+ "continue_prompt": "是否继续?(y/n)",
14
+ "manual_operation_required": "需要人工操作",
15
+ "manual_operation_hint": "请手动完成操作...",
16
+ "press_enter_when_done": "完成后按回车继续",
17
+ "connection_failed": "连接失败",
18
+ "connection_successful": "连接成功",
19
+ "step": "步骤",
20
+ "task": "任务",
21
+ "result": "结果",
22
+ "performance_metrics": "性能指标",
23
+ "time_to_first_token": "首 Token 延迟 (TTFT)",
24
+ "time_to_thinking_end": "思考完成延迟",
25
+ "total_inference_time": "总推理时间",
26
+ }
27
+
28
+ # English messages
29
+ MESSAGES_EN = {
30
+ "thinking": "Thinking",
31
+ "action": "Action",
32
+ "task_completed": "Task Completed",
33
+ "done": "Done",
34
+ "starting_task": "Starting task",
35
+ "final_result": "Final Result",
36
+ "task_result": "Task Result",
37
+ "confirmation_required": "Confirmation Required",
38
+ "continue_prompt": "Continue? (y/n)",
39
+ "manual_operation_required": "Manual Operation Required",
40
+ "manual_operation_hint": "Please complete the operation manually...",
41
+ "press_enter_when_done": "Press Enter when done",
42
+ "connection_failed": "Connection Failed",
43
+ "connection_successful": "Connection Successful",
44
+ "step": "Step",
45
+ "task": "Task",
46
+ "result": "Result",
47
+ "performance_metrics": "Performance Metrics",
48
+ "time_to_first_token": "Time to First Token (TTFT)",
49
+ "time_to_thinking_end": "Time to Thinking End",
50
+ "total_inference_time": "Total Inference Time",
51
+ }
52
+
53
+
54
+ def get_messages(lang: str = "cn") -> dict:
55
+ """
56
+ Get UI messages dictionary by language.
57
+
58
+ Args:
59
+ lang: Language code, 'cn' for Chinese, 'en' for English.
60
+
61
+ Returns:
62
+ Dictionary of UI messages.
63
+ """
64
+ if lang == "en":
65
+ return MESSAGES_EN
66
+ return MESSAGES_ZH
67
+
68
+
69
+ def get_message(key: str, lang: str = "cn") -> str:
70
+ """
71
+ Get a single UI message by key and language.
72
+
73
+ Args:
74
+ key: Message key.
75
+ lang: Language code, 'cn' for Chinese, 'en' for English.
76
+
77
+ Returns:
78
+ Message string.
79
+ """
80
+ messages = get_messages(lang)
81
+ return messages.get(key, key)
AutoGLM_GUI/metrics.py CHANGED
@@ -87,14 +87,12 @@ class AutoGLMMetricsCollector(Collector):
87
87
  busy_count = 0
88
88
 
89
89
  with manager._manager_lock:
90
- # Get snapshots (shallow copy to minimize lock time)
90
+ # Get snapshot (shallow copy to minimize lock time)
91
91
  metadata_snapshot = dict(manager._metadata)
92
- states_snapshot = dict(manager._states)
93
92
 
94
- # Iterate over _states (not _metadata) to capture failed agents
95
- for device_id, state in states_snapshot.items():
96
- # Get metadata if exists (will be None for failed initialization)
97
- metadata = metadata_snapshot.get(device_id)
93
+ # Iterate over _metadata (state is stored in AgentMetadata.state)
94
+ for device_id, metadata in metadata_snapshot.items():
95
+ state = metadata.state
98
96
 
99
97
  # Get serial from DeviceManager
100
98
  with device_manager._devices_lock:
@@ -113,20 +111,15 @@ class AutoGLMMetricsCollector(Collector):
113
111
  if state == AgentState.BUSY:
114
112
  busy_count += 1
115
113
 
116
- # Timestamps (0 if metadata doesn't exist, e.g., failed init)
117
- if metadata:
118
- last_used_gauge.add_metric(
119
- [device_id, serial],
120
- metadata.last_used,
121
- )
122
- created_gauge.add_metric(
123
- [device_id, serial],
124
- metadata.created_at,
125
- )
126
- else:
127
- # Failed initialization: report 0 timestamps
128
- last_used_gauge.add_metric([device_id, serial], 0)
129
- created_gauge.add_metric([device_id, serial], 0)
114
+ # Timestamps from metadata
115
+ last_used_gauge.add_metric(
116
+ [device_id, serial],
117
+ metadata.last_used,
118
+ )
119
+ created_gauge.add_metric(
120
+ [device_id, serial],
121
+ metadata.created_at,
122
+ )
130
123
 
131
124
  metrics.extend([agents_gauge, last_used_gauge, created_gauge])
132
125
 
@@ -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
+ ]