autoglm-gui 0.4.11__tar.gz → 0.4.13__tar.gz
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-0.4.11 → autoglm_gui-0.4.13}/.gitignore +16 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/AutoGLM_GUI/__init__.py +8 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/AutoGLM_GUI/__main__.py +29 -34
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/AutoGLM_GUI/adb_plus/__init__.py +6 -0
- autoglm_gui-0.4.13/AutoGLM_GUI/adb_plus/device.py +50 -0
- autoglm_gui-0.4.13/AutoGLM_GUI/adb_plus/ip.py +78 -0
- autoglm_gui-0.4.13/AutoGLM_GUI/adb_plus/serial.py +35 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/AutoGLM_GUI/api/__init__.py +10 -1
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/AutoGLM_GUI/api/agents.py +76 -67
- autoglm_gui-0.4.13/AutoGLM_GUI/api/devices.py +119 -0
- autoglm_gui-0.4.13/AutoGLM_GUI/api/media.py +49 -0
- autoglm_gui-0.4.13/AutoGLM_GUI/api/version.py +192 -0
- autoglm_gui-0.4.13/AutoGLM_GUI/config_manager.py +565 -0
- autoglm_gui-0.4.13/AutoGLM_GUI/exceptions.py +7 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/AutoGLM_GUI/platform_utils.py +19 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/AutoGLM_GUI/schemas.py +46 -2
- autoglm_gui-0.4.13/AutoGLM_GUI/scrcpy_protocol.py +46 -0
- autoglm_gui-0.4.13/AutoGLM_GUI/scrcpy_stream.py +459 -0
- autoglm_gui-0.4.13/AutoGLM_GUI/server.py +10 -0
- autoglm_gui-0.4.13/AutoGLM_GUI/socketio_server.py +125 -0
- autoglm_gui-0.4.11/AutoGLM_GUI/static/assets/about-wSo3UgQ-.js → autoglm_gui-0.4.13/AutoGLM_GUI/static/assets/about-29B5FDM8.js +1 -1
- autoglm_gui-0.4.13/AutoGLM_GUI/static/assets/chat-DTN2oKtA.js +149 -0
- autoglm_gui-0.4.13/AutoGLM_GUI/static/assets/index-Dy550Qqg.css +1 -0
- autoglm_gui-0.4.11/AutoGLM_GUI/static/assets/index-B5u1xtK1.js → autoglm_gui-0.4.13/AutoGLM_GUI/static/assets/index-mVNV0VwM.js +1 -1
- autoglm_gui-0.4.13/AutoGLM_GUI/static/assets/index-wu8Wjf12.js +10 -0
- autoglm_gui-0.4.13/AutoGLM_GUI/static/assets/worker-D6BRitjy.js +1 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/AutoGLM_GUI/static/index.html +2 -2
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/PKG-INFO +25 -2
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/README.md +23 -1
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/pyproject.toml +3 -1
- autoglm_gui-0.4.11/AutoGLM_GUI/api/devices.py +0 -29
- autoglm_gui-0.4.11/AutoGLM_GUI/api/media.py +0 -272
- autoglm_gui-0.4.11/AutoGLM_GUI/config_manager.py +0 -124
- autoglm_gui-0.4.11/AutoGLM_GUI/resources/apks/ADBKeyBoard.LICENSE.txt +0 -339
- autoglm_gui-0.4.11/AutoGLM_GUI/resources/apks/ADBKeyBoard.README.txt +0 -1
- autoglm_gui-0.4.11/AutoGLM_GUI/resources/apks/ADBKeyboard.apk +0 -0
- autoglm_gui-0.4.11/AutoGLM_GUI/scrcpy_stream.py +0 -574
- autoglm_gui-0.4.11/AutoGLM_GUI/server.py +0 -5
- autoglm_gui-0.4.11/AutoGLM_GUI/static/assets/chat-BcY2K0yj.js +0 -25
- autoglm_gui-0.4.11/AutoGLM_GUI/static/assets/index-CHrYo3Qj.css +0 -1
- autoglm_gui-0.4.11/AutoGLM_GUI/static/assets/index-D5BALRbT.js +0 -10
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/AutoGLM_GUI/adb_plus/keyboard_installer.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/AutoGLM_GUI/adb_plus/screenshot.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/AutoGLM_GUI/adb_plus/touch.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/AutoGLM_GUI/api/control.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/AutoGLM_GUI/config.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/AutoGLM_GUI/logger.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/AutoGLM_GUI/state.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/AutoGLM_GUI/version.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/LICENSE +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/phone_agent/__init__.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/phone_agent/actions/__init__.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/phone_agent/actions/handler.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/phone_agent/adb/__init__.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/phone_agent/adb/connection.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/phone_agent/adb/device.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/phone_agent/adb/input.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/phone_agent/adb/screenshot.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/phone_agent/agent.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/phone_agent/config/__init__.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/phone_agent/config/apps.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/phone_agent/config/i18n.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/phone_agent/config/prompts.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/phone_agent/config/prompts_en.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/phone_agent/config/prompts_zh.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/phone_agent/model/__init__.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/phone_agent/model/client.py +0 -0
- {autoglm_gui-0.4.11 → autoglm_gui-0.4.13}/scrcpy-server-v3.3.3 +0 -0
|
@@ -16,4 +16,20 @@ AutoGLM_GUI/static/
|
|
|
16
16
|
# Frontend
|
|
17
17
|
frontend/node_modules/
|
|
18
18
|
frontend/dist/
|
|
19
|
+
|
|
20
|
+
# Electron
|
|
21
|
+
electron/node_modules/
|
|
22
|
+
electron/dist/
|
|
23
|
+
|
|
24
|
+
# Build resources
|
|
25
|
+
resources/
|
|
26
|
+
|
|
27
|
+
# Logs
|
|
28
|
+
*.log
|
|
29
|
+
logs/
|
|
30
|
+
|
|
31
|
+
# MCP
|
|
19
32
|
.mcp.json
|
|
33
|
+
|
|
34
|
+
# macOS
|
|
35
|
+
.DS_Store
|
|
@@ -5,6 +5,14 @@ import sys
|
|
|
5
5
|
from functools import wraps
|
|
6
6
|
from importlib import metadata
|
|
7
7
|
|
|
8
|
+
# 修复 Windows 编码问题 - 必须在所有其他导入之前
|
|
9
|
+
if sys.platform == "win32":
|
|
10
|
+
import codecs
|
|
11
|
+
|
|
12
|
+
sys.stdout = codecs.getwriter("utf-8")(sys.stdout.buffer, "strict")
|
|
13
|
+
sys.stderr = codecs.getwriter("utf-8")(sys.stderr.buffer, "strict")
|
|
14
|
+
|
|
15
|
+
|
|
8
16
|
# ============================================================================
|
|
9
17
|
# Fix Windows encoding issue: Force UTF-8 for all subprocess calls
|
|
10
18
|
# ============================================================================
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"""CLI entry point for AutoGLM-GUI."""
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
-
import os
|
|
5
|
-
import socket
|
|
6
4
|
import sys
|
|
5
|
+
import socket
|
|
7
6
|
import threading
|
|
8
7
|
import time
|
|
9
8
|
import webbrowser
|
|
@@ -81,8 +80,8 @@ def main() -> None:
|
|
|
81
80
|
)
|
|
82
81
|
parser.add_argument(
|
|
83
82
|
"--model",
|
|
84
|
-
default=
|
|
85
|
-
help=f"Model name to use (default: {DEFAULT_MODEL_NAME})",
|
|
83
|
+
default=None,
|
|
84
|
+
help=f"Model name to use (default: {DEFAULT_MODEL_NAME}, or from config file)",
|
|
86
85
|
)
|
|
87
86
|
parser.add_argument(
|
|
88
87
|
"--apikey",
|
|
@@ -142,7 +141,7 @@ def main() -> None:
|
|
|
142
141
|
|
|
143
142
|
from AutoGLM_GUI import server
|
|
144
143
|
from AutoGLM_GUI.config import config
|
|
145
|
-
from AutoGLM_GUI.config_manager import
|
|
144
|
+
from AutoGLM_GUI.config_manager import config_manager
|
|
146
145
|
from AutoGLM_GUI.logger import configure_logger
|
|
147
146
|
|
|
148
147
|
# Configure logging system
|
|
@@ -151,35 +150,31 @@ def main() -> None:
|
|
|
151
150
|
log_file=None if args.no_log_file else args.log_file,
|
|
152
151
|
)
|
|
153
152
|
|
|
154
|
-
#
|
|
155
|
-
|
|
153
|
+
# ==================== 配置系统初始化 ====================
|
|
154
|
+
# 使用统一配置管理器(四层优先级:CLI > ENV > FILE > DEFAULT)
|
|
155
|
+
|
|
156
|
+
# 1. 设置 CLI 参数配置(最高优先级)
|
|
157
|
+
config_manager.set_cli_config(
|
|
158
|
+
base_url=args.base_url, model_name=args.model, api_key=args.apikey
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# 2. 加载环境变量配置
|
|
162
|
+
config_manager.load_env_config()
|
|
156
163
|
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
if args.base_url:
|
|
160
|
-
cli_config["base_url"] = args.base_url
|
|
161
|
-
if args.model:
|
|
162
|
-
cli_config["model_name"] = args.model
|
|
163
|
-
if args.apikey:
|
|
164
|
-
cli_config["api_key"] = args.apikey
|
|
164
|
+
# 3. 加载配置文件
|
|
165
|
+
config_manager.load_file_config()
|
|
165
166
|
|
|
166
|
-
#
|
|
167
|
-
|
|
167
|
+
# 4. 获取合并后的有效配置
|
|
168
|
+
effective_config = config_manager.get_effective_config()
|
|
168
169
|
|
|
169
|
-
#
|
|
170
|
-
|
|
171
|
-
os.environ["AUTOGLM_MODEL_NAME"] = merged_config["model_name"]
|
|
172
|
-
os.environ["AUTOGLM_API_KEY"] = merged_config["api_key"]
|
|
170
|
+
# 5. 同步到环境变量(reload 模式需要)
|
|
171
|
+
config_manager.sync_to_env()
|
|
173
172
|
|
|
174
|
-
#
|
|
173
|
+
# 6. 刷新旧的 config 对象(保持现有代码兼容)
|
|
175
174
|
config.refresh_from_env()
|
|
176
175
|
|
|
177
|
-
#
|
|
178
|
-
config_source =
|
|
179
|
-
if cli_config:
|
|
180
|
-
config_source = "CLI arguments"
|
|
181
|
-
elif file_config:
|
|
182
|
-
config_source = "config file (~/.config/autoglm/config.json)"
|
|
176
|
+
# 获取配置来源
|
|
177
|
+
config_source = config_manager.get_config_source()
|
|
183
178
|
|
|
184
179
|
# Display startup banner
|
|
185
180
|
print()
|
|
@@ -191,16 +186,16 @@ def main() -> None:
|
|
|
191
186
|
print(f" Server: http://{args.host}:{args.port}")
|
|
192
187
|
print()
|
|
193
188
|
print(" Model Configuration:")
|
|
194
|
-
print(f" Source: {config_source}")
|
|
195
|
-
print(f" Base URL: {
|
|
196
|
-
print(f" Model: {
|
|
197
|
-
if
|
|
189
|
+
print(f" Source: {config_source.value}")
|
|
190
|
+
print(f" Base URL: {effective_config.base_url or '(not set)'}")
|
|
191
|
+
print(f" Model: {effective_config.model_name}")
|
|
192
|
+
if effective_config.api_key != "EMPTY":
|
|
198
193
|
print(" API Key: (configured)")
|
|
199
194
|
print()
|
|
200
195
|
|
|
201
196
|
# Warning if base_url is not configured
|
|
202
|
-
if not
|
|
203
|
-
print("
|
|
197
|
+
if not effective_config.base_url:
|
|
198
|
+
print(" [!] WARNING: base_url is not configured!")
|
|
204
199
|
print(" Please configure via frontend or use --base-url")
|
|
205
200
|
print()
|
|
206
201
|
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
from .keyboard_installer import ADBKeyboardInstaller
|
|
4
4
|
from .screenshot import Screenshot, capture_screenshot
|
|
5
5
|
from .touch import touch_down, touch_move, touch_up
|
|
6
|
+
from .ip import get_wifi_ip
|
|
7
|
+
from .serial import get_device_serial
|
|
8
|
+
from .device import check_device_available
|
|
6
9
|
|
|
7
10
|
__all__ = [
|
|
8
11
|
"ADBKeyboardInstaller",
|
|
@@ -11,4 +14,7 @@ __all__ = [
|
|
|
11
14
|
"touch_down",
|
|
12
15
|
"touch_move",
|
|
13
16
|
"touch_up",
|
|
17
|
+
"get_wifi_ip",
|
|
18
|
+
"get_device_serial",
|
|
19
|
+
"check_device_available",
|
|
14
20
|
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Device availability checking utilities."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from AutoGLM_GUI.exceptions import DeviceNotAvailableError
|
|
6
|
+
from AutoGLM_GUI.logger import logger
|
|
7
|
+
from AutoGLM_GUI.platform_utils import run_cmd_silently
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def check_device_available(device_id: str | None = None) -> None:
|
|
11
|
+
"""Check if the device is available.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
device_id: ADB device serial (None for default device)
|
|
15
|
+
|
|
16
|
+
Raises:
|
|
17
|
+
DeviceNotAvailableError: If device is not reachable
|
|
18
|
+
"""
|
|
19
|
+
cmd = ["adb"]
|
|
20
|
+
if device_id:
|
|
21
|
+
cmd.extend(["-s", device_id])
|
|
22
|
+
cmd.append("get-state")
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
result = await asyncio.wait_for(run_cmd_silently(cmd), timeout=5.0)
|
|
26
|
+
|
|
27
|
+
state = result.stdout.strip() if result.stdout else ""
|
|
28
|
+
error_output = result.stderr.strip() if result.stderr else ""
|
|
29
|
+
|
|
30
|
+
# Check for common error patterns
|
|
31
|
+
if "not found" in error_output.lower() or "offline" in error_output.lower():
|
|
32
|
+
raise DeviceNotAvailableError(
|
|
33
|
+
f"Device {device_id} is not available: {error_output}"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if state != "device":
|
|
37
|
+
raise DeviceNotAvailableError(
|
|
38
|
+
f"Device {device_id} is not available (state: {state or 'offline'})"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
logger.debug(f"Device {device_id} is available (state: {state})")
|
|
42
|
+
|
|
43
|
+
except asyncio.TimeoutError:
|
|
44
|
+
raise DeviceNotAvailableError(f"Device {device_id} connection timed out")
|
|
45
|
+
except FileNotFoundError:
|
|
46
|
+
raise DeviceNotAvailableError("ADB executable not found")
|
|
47
|
+
except DeviceNotAvailableError:
|
|
48
|
+
raise
|
|
49
|
+
except Exception as e:
|
|
50
|
+
raise DeviceNotAvailableError(f"Failed to check device {device_id}: {e}")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""ADB IP helpers (prefer WiFi address, skip cellular interfaces)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
__all__ = ["get_wifi_ip"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _run(adb_path: str, device_id: Optional[str], cmd: list[str]) -> str:
|
|
13
|
+
base_cmd = [adb_path]
|
|
14
|
+
if device_id:
|
|
15
|
+
base_cmd.extend(["-s", device_id])
|
|
16
|
+
result = subprocess.run(
|
|
17
|
+
base_cmd + ["shell", *cmd], capture_output=True, text=True, timeout=5
|
|
18
|
+
)
|
|
19
|
+
return (result.stdout or "") + (result.stderr or "")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _extract_ip(text: str) -> Optional[str]:
|
|
23
|
+
m = re.search(r"\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b", text)
|
|
24
|
+
if not m:
|
|
25
|
+
return None
|
|
26
|
+
ip = m.group(0)
|
|
27
|
+
if ip == "0.0.0.0":
|
|
28
|
+
return None
|
|
29
|
+
return ip
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_wifi_ip(
|
|
33
|
+
adb_path: str = "adb", device_id: Optional[str] = None
|
|
34
|
+
) -> Optional[str]:
|
|
35
|
+
"""
|
|
36
|
+
Prefer WiFi IP when multiple interfaces exist.
|
|
37
|
+
|
|
38
|
+
- First try `ip -4 route get 8.8.8.8`, skip typical cellular interfaces (ccmni/rmnet).
|
|
39
|
+
- Fallback to `ip -4 addr show wlan0`.
|
|
40
|
+
Returns None if no suitable IP is found or on error.
|
|
41
|
+
"""
|
|
42
|
+
# 1) route
|
|
43
|
+
try:
|
|
44
|
+
route_out = _run(adb_path, device_id, ["ip", "-4", "route", "get", "8.8.8.8"])
|
|
45
|
+
for line in route_out.splitlines():
|
|
46
|
+
if "src" not in line:
|
|
47
|
+
continue
|
|
48
|
+
parts = line.split()
|
|
49
|
+
iface = None
|
|
50
|
+
ip = None
|
|
51
|
+
if "dev" in parts:
|
|
52
|
+
try:
|
|
53
|
+
iface = parts[parts.index("dev") + 1]
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
if "src" in parts:
|
|
57
|
+
try:
|
|
58
|
+
ip = parts[parts.index("src") + 1]
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
if not ip or ip == "0.0.0.0":
|
|
62
|
+
continue
|
|
63
|
+
if iface and (iface.startswith("ccmni") or iface.startswith("rmnet")):
|
|
64
|
+
continue
|
|
65
|
+
return ip
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
# 2) wlan0 addr
|
|
70
|
+
try:
|
|
71
|
+
addr_out = _run(adb_path, device_id, ["ip", "-4", "addr", "show", "wlan0"])
|
|
72
|
+
ip = _extract_ip(addr_out)
|
|
73
|
+
if ip:
|
|
74
|
+
return ip
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
return None
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Get device serial number using ADB."""
|
|
2
|
+
|
|
3
|
+
from AutoGLM_GUI.platform_utils import run_cmd_silently_sync
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_device_serial(device_id: str, adb_path: str = "adb") -> str | None:
|
|
7
|
+
"""
|
|
8
|
+
Get the real hardware serial number of a device.
|
|
9
|
+
|
|
10
|
+
This works for both USB and WiFi connected devices,
|
|
11
|
+
returning the actual hardware serial number (ro.serialno).
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
device_id: The device ID (can be USB serial or IP:port for WiFi)
|
|
15
|
+
adb_path: Path to adb executable (default: "adb")
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
The device hardware serial number, or None if failed
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
# Use getprop to get the actual hardware serial number
|
|
22
|
+
# This works for both USB and WiFi connections
|
|
23
|
+
result = run_cmd_silently_sync(
|
|
24
|
+
[adb_path, "-s", device_id, "shell", "getprop", "ro.serialno"],
|
|
25
|
+
timeout=3,
|
|
26
|
+
)
|
|
27
|
+
if result.returncode == 0:
|
|
28
|
+
serial = result.stdout.strip()
|
|
29
|
+
# Filter out error messages and empty values
|
|
30
|
+
if serial and not serial.startswith("error:") and serial != "unknown":
|
|
31
|
+
return serial
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
return None
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""FastAPI application factory and route registration."""
|
|
2
2
|
|
|
3
|
+
import sys
|
|
3
4
|
from importlib.resources import files
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
@@ -10,11 +11,18 @@ from fastapi.staticfiles import StaticFiles
|
|
|
10
11
|
|
|
11
12
|
from AutoGLM_GUI.version import APP_VERSION
|
|
12
13
|
|
|
13
|
-
from . import agents, control, devices, media
|
|
14
|
+
from . import agents, control, devices, media, version
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def _get_static_dir() -> Path | None:
|
|
17
18
|
"""Locate packaged static assets."""
|
|
19
|
+
# Priority 1: PyInstaller bundled path (for packaged executable)
|
|
20
|
+
if getattr(sys, "_MEIPASS", None):
|
|
21
|
+
bundled_static = Path(sys._MEIPASS) / "AutoGLM_GUI" / "static"
|
|
22
|
+
if bundled_static.exists():
|
|
23
|
+
return bundled_static
|
|
24
|
+
|
|
25
|
+
# Priority 2: importlib.resources (for installed package)
|
|
18
26
|
try:
|
|
19
27
|
static_dir = files("AutoGLM_GUI").joinpath("static")
|
|
20
28
|
if hasattr(static_dir, "_path"):
|
|
@@ -46,6 +54,7 @@ def create_app() -> FastAPI:
|
|
|
46
54
|
app.include_router(devices.router)
|
|
47
55
|
app.include_router(control.router)
|
|
48
56
|
app.include_router(media.router)
|
|
57
|
+
app.include_router(version.router)
|
|
49
58
|
|
|
50
59
|
static_dir = _get_static_dir()
|
|
51
60
|
if static_dir is not None and static_dir.exists():
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
"""Agent lifecycle and chat routes."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
import os
|
|
5
4
|
|
|
6
5
|
from fastapi import APIRouter, HTTPException
|
|
7
6
|
from fastapi.responses import StreamingResponse
|
|
7
|
+
from pydantic import ValidationError
|
|
8
8
|
|
|
9
9
|
from AutoGLM_GUI.config import config
|
|
10
|
-
from AutoGLM_GUI.config_manager import (
|
|
11
|
-
delete_config_file,
|
|
12
|
-
get_config_path,
|
|
13
|
-
load_config_file,
|
|
14
|
-
save_config_file,
|
|
15
|
-
)
|
|
16
10
|
from AutoGLM_GUI.schemas import (
|
|
17
11
|
APIAgentConfig,
|
|
18
12
|
APIModelConfig,
|
|
@@ -41,6 +35,7 @@ router = APIRouter()
|
|
|
41
35
|
def init_agent(request: InitRequest) -> dict:
|
|
42
36
|
"""初始化 PhoneAgent(多设备支持)。"""
|
|
43
37
|
from AutoGLM_GUI.adb_plus import ADBKeyboardInstaller
|
|
38
|
+
from AutoGLM_GUI.config_manager import config_manager
|
|
44
39
|
from AutoGLM_GUI.logger import logger
|
|
45
40
|
|
|
46
41
|
req_model_config = request.model or APIModelConfig()
|
|
@@ -51,6 +46,10 @@ def init_agent(request: InitRequest) -> dict:
|
|
|
51
46
|
raise HTTPException(
|
|
52
47
|
status_code=400, detail="device_id is required in agent_config"
|
|
53
48
|
)
|
|
49
|
+
|
|
50
|
+
# 热重载配置文件(支持运行时手动修改)
|
|
51
|
+
config_manager.load_file_config()
|
|
52
|
+
config_manager.sync_to_env()
|
|
54
53
|
config.refresh_from_env()
|
|
55
54
|
|
|
56
55
|
# 检查并自动安装 ADB Keyboard
|
|
@@ -261,80 +260,86 @@ def reset_agent(request: ResetRequest) -> dict:
|
|
|
261
260
|
@router.get("/api/config", response_model=ConfigResponse)
|
|
262
261
|
def get_config_endpoint() -> ConfigResponse:
|
|
263
262
|
"""获取当前有效配置."""
|
|
264
|
-
from AutoGLM_GUI.
|
|
265
|
-
|
|
266
|
-
# 加载配置文件
|
|
267
|
-
file_config = load_config_file()
|
|
268
|
-
|
|
269
|
-
# 读取当前实际运行的配置
|
|
270
|
-
current_base_url = os.getenv("AUTOGLM_BASE_URL", config.base_url)
|
|
271
|
-
current_model_name = os.getenv("AUTOGLM_MODEL_NAME", config.model_name)
|
|
272
|
-
current_api_key = os.getenv("AUTOGLM_API_KEY", config.api_key)
|
|
273
|
-
|
|
274
|
-
# 判断配置来源
|
|
275
|
-
# 如果环境变量中有 CLI 参数设置的值,优先级最高
|
|
276
|
-
env_config = {
|
|
277
|
-
"base_url": os.getenv("AUTOGLM_BASE_URL"),
|
|
278
|
-
"model_name": os.getenv("AUTOGLM_MODEL_NAME"),
|
|
279
|
-
"api_key": os.getenv("AUTOGLM_API_KEY"),
|
|
280
|
-
}
|
|
263
|
+
from AutoGLM_GUI.config_manager import config_manager
|
|
281
264
|
|
|
282
|
-
#
|
|
283
|
-
|
|
284
|
-
(env_config["base_url"] and env_config["base_url"] != "") and
|
|
285
|
-
(not file_config or file_config.get("base_url") != env_config["base_url"])
|
|
286
|
-
) or (
|
|
287
|
-
(env_config["model_name"] and env_config["model_name"] != "autoglm-phone-9b") and
|
|
288
|
-
(not file_config or file_config.get("model_name") != env_config["model_name"])
|
|
289
|
-
) or (
|
|
290
|
-
(env_config["api_key"] and env_config["api_key"] != "EMPTY") and
|
|
291
|
-
(not file_config or file_config.get("api_key") != env_config["api_key"])
|
|
292
|
-
)
|
|
265
|
+
# 热重载:检查文件是否被外部修改
|
|
266
|
+
config_manager.load_file_config()
|
|
293
267
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
268
|
+
# 获取有效配置和来源
|
|
269
|
+
effective_config = config_manager.get_effective_config()
|
|
270
|
+
source = config_manager.get_config_source()
|
|
271
|
+
|
|
272
|
+
# 检测冲突
|
|
273
|
+
conflicts = config_manager.detect_conflicts()
|
|
300
274
|
|
|
301
275
|
return ConfigResponse(
|
|
302
|
-
base_url=
|
|
303
|
-
model_name=
|
|
304
|
-
api_key=
|
|
305
|
-
source=source,
|
|
276
|
+
base_url=effective_config.base_url,
|
|
277
|
+
model_name=effective_config.model_name,
|
|
278
|
+
api_key=effective_config.api_key if effective_config.api_key != "EMPTY" else "",
|
|
279
|
+
source=source.value,
|
|
280
|
+
conflicts=[
|
|
281
|
+
{
|
|
282
|
+
"field": c.field,
|
|
283
|
+
"file_value": c.file_value,
|
|
284
|
+
"override_value": c.override_value,
|
|
285
|
+
"override_source": c.override_source.value,
|
|
286
|
+
}
|
|
287
|
+
for c in conflicts
|
|
288
|
+
]
|
|
289
|
+
if conflicts
|
|
290
|
+
else None,
|
|
306
291
|
)
|
|
307
292
|
|
|
308
293
|
|
|
309
294
|
@router.post("/api/config")
|
|
310
295
|
def save_config_endpoint(request: ConfigSaveRequest) -> dict:
|
|
311
296
|
"""保存配置到文件."""
|
|
297
|
+
from AutoGLM_GUI.config_manager import ConfigModel, config_manager
|
|
298
|
+
|
|
312
299
|
try:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
300
|
+
# Validate incoming configuration to avoid silently falling back to defaults
|
|
301
|
+
ConfigModel(
|
|
302
|
+
base_url=request.base_url,
|
|
303
|
+
model_name=request.model_name,
|
|
304
|
+
api_key=request.api_key or "EMPTY",
|
|
305
|
+
)
|
|
317
306
|
|
|
318
|
-
#
|
|
319
|
-
|
|
320
|
-
|
|
307
|
+
# 保存配置(合并模式,不丢失字段)
|
|
308
|
+
success = config_manager.save_file_config(
|
|
309
|
+
base_url=request.base_url,
|
|
310
|
+
model_name=request.model_name,
|
|
311
|
+
api_key=request.api_key,
|
|
312
|
+
merge_mode=True,
|
|
313
|
+
)
|
|
321
314
|
|
|
322
|
-
|
|
315
|
+
if not success:
|
|
316
|
+
raise HTTPException(status_code=500, detail="Failed to save config")
|
|
323
317
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
os.environ["AUTOGLM_MODEL_NAME"] = request.model_name
|
|
328
|
-
if request.api_key:
|
|
329
|
-
os.environ["AUTOGLM_API_KEY"] = request.api_key
|
|
330
|
-
config.refresh_from_env()
|
|
318
|
+
# 同步到环境变量
|
|
319
|
+
config_manager.sync_to_env()
|
|
320
|
+
config.refresh_from_env()
|
|
331
321
|
|
|
322
|
+
# 检测冲突并返回警告
|
|
323
|
+
conflicts = config_manager.detect_conflicts()
|
|
324
|
+
|
|
325
|
+
if conflicts:
|
|
326
|
+
warnings = [
|
|
327
|
+
f"{c.field}: file value overridden by {c.override_source.value}"
|
|
328
|
+
for c in conflicts
|
|
329
|
+
]
|
|
332
330
|
return {
|
|
333
331
|
"success": True,
|
|
334
|
-
"message": f"Configuration saved to {get_config_path()}",
|
|
332
|
+
"message": f"Configuration saved to {config_manager.get_config_path()}",
|
|
333
|
+
"warnings": warnings,
|
|
335
334
|
}
|
|
336
|
-
|
|
337
|
-
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
"success": True,
|
|
338
|
+
"message": f"Configuration saved to {config_manager.get_config_path()}",
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
except ValidationError as e:
|
|
342
|
+
raise HTTPException(status_code=400, detail=f"Invalid configuration: {e}")
|
|
338
343
|
except Exception as e:
|
|
339
344
|
raise HTTPException(status_code=500, detail=str(e))
|
|
340
345
|
|
|
@@ -342,11 +347,15 @@ def save_config_endpoint(request: ConfigSaveRequest) -> dict:
|
|
|
342
347
|
@router.delete("/api/config")
|
|
343
348
|
def delete_config_endpoint() -> dict:
|
|
344
349
|
"""删除配置文件."""
|
|
350
|
+
from AutoGLM_GUI.config_manager import config_manager
|
|
351
|
+
|
|
345
352
|
try:
|
|
346
|
-
success =
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
else:
|
|
353
|
+
success = config_manager.delete_file_config()
|
|
354
|
+
|
|
355
|
+
if not success:
|
|
350
356
|
raise HTTPException(status_code=500, detail="Failed to delete config")
|
|
357
|
+
|
|
358
|
+
return {"success": True, "message": "Configuration deleted"}
|
|
359
|
+
|
|
351
360
|
except Exception as e:
|
|
352
361
|
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Device discovery routes."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
|
|
5
|
+
from AutoGLM_GUI.adb_plus import get_wifi_ip, get_device_serial
|
|
6
|
+
|
|
7
|
+
from AutoGLM_GUI.schemas import (
|
|
8
|
+
DeviceListResponse,
|
|
9
|
+
WiFiConnectRequest,
|
|
10
|
+
WiFiConnectResponse,
|
|
11
|
+
WiFiDisconnectRequest,
|
|
12
|
+
WiFiDisconnectResponse,
|
|
13
|
+
)
|
|
14
|
+
from AutoGLM_GUI.state import agents
|
|
15
|
+
|
|
16
|
+
router = APIRouter()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.get("/api/devices", response_model=DeviceListResponse)
|
|
20
|
+
def list_devices() -> DeviceListResponse:
|
|
21
|
+
"""列出所有 ADB 设备。"""
|
|
22
|
+
from phone_agent.adb import list_devices as adb_list, ADBConnection
|
|
23
|
+
|
|
24
|
+
adb_devices = adb_list()
|
|
25
|
+
conn = ADBConnection()
|
|
26
|
+
|
|
27
|
+
devices_with_serial = []
|
|
28
|
+
for d in adb_devices:
|
|
29
|
+
# 使用 adb_plus 的 get_device_serial 获取真实序列号
|
|
30
|
+
serial = get_device_serial(d.device_id, conn.adb_path)
|
|
31
|
+
|
|
32
|
+
devices_with_serial.append(
|
|
33
|
+
{
|
|
34
|
+
"id": d.device_id,
|
|
35
|
+
"model": d.model or "Unknown",
|
|
36
|
+
"status": d.status,
|
|
37
|
+
"connection_type": d.connection_type.value,
|
|
38
|
+
"is_initialized": d.device_id in agents,
|
|
39
|
+
"serial": serial, # 真实序列号
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return DeviceListResponse(devices=devices_with_serial)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@router.post("/api/devices/connect_wifi", response_model=WiFiConnectResponse)
|
|
47
|
+
def connect_wifi(request: WiFiConnectRequest) -> WiFiConnectResponse:
|
|
48
|
+
"""从 USB 启用 TCP/IP 并连接到 WiFi。"""
|
|
49
|
+
from phone_agent.adb import ADBConnection, ConnectionType
|
|
50
|
+
|
|
51
|
+
conn = ADBConnection()
|
|
52
|
+
|
|
53
|
+
# 优先使用传入的 device_id,否则取第一个在线设备
|
|
54
|
+
device_info = conn.get_device_info(request.device_id)
|
|
55
|
+
if not device_info:
|
|
56
|
+
return WiFiConnectResponse(
|
|
57
|
+
success=False,
|
|
58
|
+
message="No connected device found",
|
|
59
|
+
error="device_not_found",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# 已经是 WiFi 连接则直接返回
|
|
63
|
+
if device_info.connection_type == ConnectionType.REMOTE:
|
|
64
|
+
address = device_info.device_id
|
|
65
|
+
return WiFiConnectResponse(
|
|
66
|
+
success=True,
|
|
67
|
+
message="Already connected over WiFi",
|
|
68
|
+
device_id=address,
|
|
69
|
+
address=address,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# 1) 启用 tcpip
|
|
73
|
+
ok, msg = conn.enable_tcpip(port=request.port, device_id=device_info.device_id)
|
|
74
|
+
if not ok:
|
|
75
|
+
return WiFiConnectResponse(
|
|
76
|
+
success=False, message=msg or "Failed to enable tcpip", error="tcpip"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# 2) 读取设备 IP:先用本地 adb_plus 的 WiFi 优先逻辑,失败再回退上游接口
|
|
80
|
+
ip = get_wifi_ip(conn.adb_path, device_info.device_id) or conn.get_device_ip(
|
|
81
|
+
device_info.device_id
|
|
82
|
+
)
|
|
83
|
+
if not ip:
|
|
84
|
+
return WiFiConnectResponse(
|
|
85
|
+
success=False, message="Failed to get device IP", error="ip"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
address = f"{ip}:{request.port}"
|
|
89
|
+
|
|
90
|
+
# 3) 连接 WiFi
|
|
91
|
+
ok, msg = conn.connect(address)
|
|
92
|
+
if not ok:
|
|
93
|
+
return WiFiConnectResponse(
|
|
94
|
+
success=False,
|
|
95
|
+
message=msg or "Failed to connect over WiFi",
|
|
96
|
+
error="connect",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return WiFiConnectResponse(
|
|
100
|
+
success=True,
|
|
101
|
+
message="Switched to WiFi successfully",
|
|
102
|
+
device_id=address,
|
|
103
|
+
address=address,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@router.post("/api/devices/disconnect_wifi", response_model=WiFiDisconnectResponse)
|
|
108
|
+
def disconnect_wifi(request: WiFiDisconnectRequest) -> WiFiDisconnectResponse:
|
|
109
|
+
"""断开 WiFi 连接。"""
|
|
110
|
+
from phone_agent.adb import ADBConnection
|
|
111
|
+
|
|
112
|
+
conn = ADBConnection()
|
|
113
|
+
ok, msg = conn.disconnect(request.device_id)
|
|
114
|
+
|
|
115
|
+
return WiFiDisconnectResponse(
|
|
116
|
+
success=ok,
|
|
117
|
+
message=msg,
|
|
118
|
+
error=None if ok else "disconnect_failed",
|
|
119
|
+
)
|