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.
- AutoGLM_GUI/__init__.py +11 -0
- AutoGLM_GUI/__main__.py +26 -8
- AutoGLM_GUI/actions/__init__.py +6 -0
- AutoGLM_GUI/actions/handler.py +196 -0
- AutoGLM_GUI/actions/types.py +15 -0
- AutoGLM_GUI/adb/__init__.py +53 -0
- AutoGLM_GUI/adb/apps.py +227 -0
- AutoGLM_GUI/adb/connection.py +323 -0
- AutoGLM_GUI/adb/device.py +171 -0
- AutoGLM_GUI/adb/input.py +67 -0
- AutoGLM_GUI/adb/screenshot.py +11 -0
- AutoGLM_GUI/adb/timing.py +167 -0
- AutoGLM_GUI/adb_plus/keyboard_installer.py +4 -2
- AutoGLM_GUI/adb_plus/qr_pair.py +8 -8
- AutoGLM_GUI/adb_plus/screenshot.py +22 -1
- AutoGLM_GUI/adb_plus/serial.py +38 -20
- AutoGLM_GUI/adb_plus/touch.py +4 -9
- AutoGLM_GUI/agents/__init__.py +51 -0
- AutoGLM_GUI/agents/events.py +19 -0
- AutoGLM_GUI/agents/factory.py +153 -0
- AutoGLM_GUI/agents/glm/__init__.py +7 -0
- AutoGLM_GUI/agents/glm/agent.py +292 -0
- AutoGLM_GUI/agents/glm/message_builder.py +81 -0
- AutoGLM_GUI/agents/glm/parser.py +110 -0
- AutoGLM_GUI/agents/glm/prompts_en.py +77 -0
- AutoGLM_GUI/agents/glm/prompts_zh.py +75 -0
- AutoGLM_GUI/agents/mai/__init__.py +28 -0
- AutoGLM_GUI/agents/mai/agent.py +405 -0
- AutoGLM_GUI/agents/mai/parser.py +254 -0
- AutoGLM_GUI/agents/mai/prompts.py +103 -0
- AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
- AutoGLM_GUI/agents/protocols.py +27 -0
- AutoGLM_GUI/agents/stream_runner.py +188 -0
- AutoGLM_GUI/api/__init__.py +71 -11
- AutoGLM_GUI/api/agents.py +190 -229
- AutoGLM_GUI/api/control.py +9 -6
- AutoGLM_GUI/api/devices.py +112 -28
- AutoGLM_GUI/api/health.py +13 -0
- AutoGLM_GUI/api/history.py +78 -0
- AutoGLM_GUI/api/layered_agent.py +306 -181
- AutoGLM_GUI/api/mcp.py +11 -10
- AutoGLM_GUI/api/media.py +64 -1
- AutoGLM_GUI/api/scheduled_tasks.py +98 -0
- AutoGLM_GUI/api/version.py +23 -10
- AutoGLM_GUI/api/workflows.py +2 -1
- AutoGLM_GUI/config.py +72 -14
- AutoGLM_GUI/config_manager.py +98 -27
- AutoGLM_GUI/device_adapter.py +263 -0
- AutoGLM_GUI/device_manager.py +248 -29
- AutoGLM_GUI/device_protocol.py +266 -0
- AutoGLM_GUI/devices/__init__.py +49 -0
- AutoGLM_GUI/devices/adb_device.py +200 -0
- AutoGLM_GUI/devices/mock_device.py +185 -0
- AutoGLM_GUI/devices/remote_device.py +177 -0
- AutoGLM_GUI/exceptions.py +3 -3
- AutoGLM_GUI/history_manager.py +164 -0
- AutoGLM_GUI/i18n.py +81 -0
- AutoGLM_GUI/metrics.py +13 -20
- AutoGLM_GUI/model/__init__.py +5 -0
- AutoGLM_GUI/model/message_builder.py +69 -0
- AutoGLM_GUI/model/types.py +24 -0
- AutoGLM_GUI/models/__init__.py +10 -0
- AutoGLM_GUI/models/history.py +96 -0
- AutoGLM_GUI/models/scheduled_task.py +71 -0
- AutoGLM_GUI/parsers/__init__.py +22 -0
- AutoGLM_GUI/parsers/base.py +50 -0
- AutoGLM_GUI/parsers/phone_parser.py +58 -0
- AutoGLM_GUI/phone_agent_manager.py +118 -367
- AutoGLM_GUI/platform_utils.py +31 -2
- AutoGLM_GUI/prompt_config.py +15 -0
- AutoGLM_GUI/prompts/__init__.py +32 -0
- AutoGLM_GUI/scheduler_manager.py +304 -0
- AutoGLM_GUI/schemas.py +272 -63
- AutoGLM_GUI/scrcpy_stream.py +159 -37
- AutoGLM_GUI/server.py +3 -1
- AutoGLM_GUI/socketio_server.py +114 -29
- AutoGLM_GUI/state.py +10 -30
- AutoGLM_GUI/static/assets/{about-DeclntHg.js → about-BQm96DAl.js} +1 -1
- AutoGLM_GUI/static/assets/alert-dialog-B42XxGPR.js +1 -0
- AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +129 -0
- AutoGLM_GUI/static/assets/circle-alert-D4rSJh37.js +1 -0
- AutoGLM_GUI/static/assets/dialog-DZ78cEcj.js +45 -0
- AutoGLM_GUI/static/assets/history-DFBv7TGc.js +1 -0
- AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +1 -0
- AutoGLM_GUI/static/assets/{index-zQ4KKDHt.js → index-CmZSnDqc.js} +1 -1
- AutoGLM_GUI/static/assets/index-CssG-3TH.js +11 -0
- AutoGLM_GUI/static/assets/label-BCUzE_nm.js +1 -0
- AutoGLM_GUI/static/assets/logs-eoFxn5of.js +1 -0
- AutoGLM_GUI/static/assets/popover-DLsuV5Sx.js +1 -0
- AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +1 -0
- AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +1 -0
- AutoGLM_GUI/static/assets/textarea-BX6y7uM5.js +1 -0
- AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +1 -0
- AutoGLM_GUI/static/index.html +2 -2
- AutoGLM_GUI/types.py +142 -0
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/METADATA +178 -92
- autoglm_gui-1.5.0.dist-info/RECORD +157 -0
- mai_agent/base.py +137 -0
- mai_agent/mai_grounding_agent.py +263 -0
- mai_agent/mai_naivigation_agent.py +526 -0
- mai_agent/prompt.py +148 -0
- mai_agent/unified_memory.py +67 -0
- mai_agent/utils.py +73 -0
- AutoGLM_GUI/api/dual_model.py +0 -311
- AutoGLM_GUI/dual_model/__init__.py +0 -53
- AutoGLM_GUI/dual_model/decision_model.py +0 -664
- AutoGLM_GUI/dual_model/dual_agent.py +0 -917
- AutoGLM_GUI/dual_model/protocols.py +0 -354
- AutoGLM_GUI/dual_model/vision_model.py +0 -442
- AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
- AutoGLM_GUI/phone_agent_patches.py +0 -146
- AutoGLM_GUI/static/assets/chat-Iut2yhSw.js +0 -125
- AutoGLM_GUI/static/assets/dialog-BfdcBs1x.js +0 -45
- AutoGLM_GUI/static/assets/index-5hCCwHA7.css +0 -1
- AutoGLM_GUI/static/assets/index-DHF1NZh0.js +0 -12
- AutoGLM_GUI/static/assets/workflows-xiplap-r.js +0 -1
- autoglm_gui-1.4.0.dist-info/RECORD +0 -100
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
>>>
|
|
83
|
-
>>> print(f"base_url: {
|
|
84
|
-
>>> print(f"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
|
|
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
|
|
95
|
-
for device_id,
|
|
96
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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,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
|