autoglm-gui 0.4.11__py3-none-any.whl → 0.4.13__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 +8 -0
- AutoGLM_GUI/__main__.py +29 -34
- AutoGLM_GUI/adb_plus/__init__.py +6 -0
- AutoGLM_GUI/adb_plus/device.py +50 -0
- AutoGLM_GUI/adb_plus/ip.py +78 -0
- AutoGLM_GUI/adb_plus/serial.py +35 -0
- AutoGLM_GUI/api/__init__.py +10 -1
- AutoGLM_GUI/api/agents.py +76 -67
- AutoGLM_GUI/api/devices.py +96 -6
- AutoGLM_GUI/api/media.py +12 -235
- AutoGLM_GUI/api/version.py +192 -0
- AutoGLM_GUI/config_manager.py +538 -97
- AutoGLM_GUI/exceptions.py +7 -0
- AutoGLM_GUI/platform_utils.py +19 -0
- AutoGLM_GUI/schemas.py +46 -2
- AutoGLM_GUI/scrcpy_protocol.py +46 -0
- AutoGLM_GUI/scrcpy_stream.py +192 -307
- AutoGLM_GUI/server.py +7 -2
- AutoGLM_GUI/socketio_server.py +125 -0
- AutoGLM_GUI/static/assets/{about-wSo3UgQ-.js → about-29B5FDM8.js} +1 -1
- AutoGLM_GUI/static/assets/chat-DTN2oKtA.js +149 -0
- AutoGLM_GUI/static/assets/index-Dy550Qqg.css +1 -0
- AutoGLM_GUI/static/assets/{index-B5u1xtK1.js → index-mVNV0VwM.js} +1 -1
- AutoGLM_GUI/static/assets/index-wu8Wjf12.js +10 -0
- AutoGLM_GUI/static/assets/worker-D6BRitjy.js +1 -0
- AutoGLM_GUI/static/index.html +2 -2
- {autoglm_gui-0.4.11.dist-info → autoglm_gui-0.4.13.dist-info}/METADATA +25 -2
- autoglm_gui-0.4.13.dist-info/RECORD +57 -0
- AutoGLM_GUI/resources/apks/ADBKeyBoard.LICENSE.txt +0 -339
- AutoGLM_GUI/resources/apks/ADBKeyBoard.README.txt +0 -1
- AutoGLM_GUI/resources/apks/ADBKeyboard.apk +0 -0
- AutoGLM_GUI/static/assets/chat-BcY2K0yj.js +0 -25
- AutoGLM_GUI/static/assets/index-CHrYo3Qj.css +0 -1
- AutoGLM_GUI/static/assets/index-D5BALRbT.js +0 -10
- autoglm_gui-0.4.11.dist-info/RECORD +0 -52
- {autoglm_gui-0.4.11.dist-info → autoglm_gui-0.4.13.dist-info}/WHEEL +0 -0
- {autoglm_gui-0.4.11.dist-info → autoglm_gui-0.4.13.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-0.4.11.dist-info → autoglm_gui-0.4.13.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/config_manager.py
CHANGED
|
@@ -1,124 +1,565 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""统一配置管理模块 - 四层优先级系统.
|
|
2
|
+
|
|
3
|
+
配置优先级:CLI 参数 > 环境变量 > 配置文件 > 默认值
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
- 类型安全的配置模型(Pydantic 验证)
|
|
7
|
+
- 多层配置系统,带源追踪
|
|
8
|
+
- 配置冲突检测和提示
|
|
9
|
+
- 配置文件热重载(基于 mtime 缓存)
|
|
10
|
+
- 原子文件写入
|
|
11
|
+
- 环境变量同步(支持 --reload 模式)
|
|
12
|
+
"""
|
|
2
13
|
|
|
3
14
|
import json
|
|
15
|
+
import os
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from enum import Enum
|
|
4
18
|
from pathlib import Path
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, field_validator
|
|
5
22
|
|
|
6
23
|
from AutoGLM_GUI.logger import logger
|
|
7
24
|
|
|
8
|
-
# 默认配置
|
|
9
|
-
DEFAULT_CONFIG = {"base_url": "", "model_name": "autoglm-phone-9b", "api_key": "EMPTY"}
|
|
10
25
|
|
|
26
|
+
# ==================== 配置源枚举 ====================
|
|
11
27
|
|
|
12
|
-
def get_config_path() -> Path:
|
|
13
|
-
"""获取配置文件路径.
|
|
14
28
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
29
|
+
class ConfigSource(str, Enum):
|
|
30
|
+
"""配置来源枚举(按优先级从高到低)."""
|
|
31
|
+
|
|
32
|
+
CLI = "CLI arguments"
|
|
33
|
+
ENV = "environment variables"
|
|
34
|
+
FILE = "config file (~/.config/autoglm/config.json)"
|
|
35
|
+
DEFAULT = "default"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ==================== 类型安全配置模型 ====================
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ConfigModel(BaseModel):
|
|
42
|
+
"""类型安全的配置模型,使用 Pydantic 进行验证."""
|
|
43
|
+
|
|
44
|
+
base_url: str = ""
|
|
45
|
+
model_name: str = "autoglm-phone-9b"
|
|
46
|
+
api_key: str = "EMPTY"
|
|
47
|
+
|
|
48
|
+
@field_validator("base_url")
|
|
49
|
+
@classmethod
|
|
50
|
+
def validate_base_url(cls, v: str) -> str:
|
|
51
|
+
"""验证 base_url 格式."""
|
|
52
|
+
if v and not v.startswith(("http://", "https://")):
|
|
53
|
+
raise ValueError("base_url must start with http:// or https://")
|
|
54
|
+
return v.rstrip("/") # 去除尾部斜杠
|
|
55
|
+
|
|
56
|
+
@field_validator("model_name")
|
|
57
|
+
@classmethod
|
|
58
|
+
def validate_model_name(cls, v: str) -> str:
|
|
59
|
+
"""验证 model_name 非空."""
|
|
60
|
+
if not v or not v.strip():
|
|
61
|
+
raise ValueError("model_name cannot be empty")
|
|
62
|
+
return v.strip()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ==================== 配置层数据类 ====================
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class ConfigLayer:
|
|
70
|
+
"""单个配置层,带源追踪."""
|
|
71
|
+
|
|
72
|
+
base_url: Optional[str] = None
|
|
73
|
+
model_name: Optional[str] = None
|
|
74
|
+
api_key: Optional[str] = None
|
|
75
|
+
source: ConfigSource = ConfigSource.DEFAULT
|
|
76
|
+
|
|
77
|
+
def has_value(self, key: str) -> bool:
|
|
78
|
+
"""检查此层是否有非 None 的值.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
key: 配置键名
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
bool: 如果有值返回 True
|
|
85
|
+
"""
|
|
86
|
+
value = getattr(self, key, None)
|
|
87
|
+
return value is not None
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> dict:
|
|
90
|
+
"""转换为字典,排除 None 值.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
dict: 配置字典
|
|
94
|
+
"""
|
|
95
|
+
return {
|
|
96
|
+
k: v
|
|
97
|
+
for k, v in {
|
|
98
|
+
"base_url": self.base_url,
|
|
99
|
+
"model_name": self.model_name,
|
|
100
|
+
"api_key": self.api_key,
|
|
101
|
+
}.items()
|
|
102
|
+
if v is not None
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ==================== 配置冲突数据类 ====================
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class ConfigConflict:
|
|
111
|
+
"""配置冲突信息."""
|
|
20
112
|
|
|
113
|
+
field: str # 冲突的字段名
|
|
114
|
+
file_value: Optional[str] # 配置文件中的值
|
|
115
|
+
override_value: str # 覆盖的值
|
|
116
|
+
override_source: ConfigSource # 覆盖来源(CLI 或 ENV)
|
|
21
117
|
|
|
22
|
-
def load_config_file() -> dict | None:
|
|
23
|
-
"""从文件加载配置.
|
|
24
118
|
|
|
25
|
-
|
|
26
|
-
|
|
119
|
+
# ==================== 统一配置管理器 ====================
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class UnifiedConfigManager:
|
|
27
123
|
"""
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return config
|
|
40
|
-
except json.JSONDecodeError as e:
|
|
41
|
-
logger.warning(f"Failed to parse config file {config_path}: {e}")
|
|
42
|
-
return None
|
|
43
|
-
except Exception as e:
|
|
44
|
-
logger.error(f"Failed to read config file {config_path}: {e}")
|
|
45
|
-
return None
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def save_config_file(config: dict) -> bool:
|
|
49
|
-
"""保存配置到文件(原子写入).
|
|
50
|
-
|
|
51
|
-
Args:
|
|
52
|
-
config: 配置字典
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
bool: 成功返回 True,失败返回 False
|
|
124
|
+
统一配置管理器(单例模式).
|
|
125
|
+
|
|
126
|
+
配置优先级:CLI 参数 > 环境变量 > 配置文件 > 默认值
|
|
127
|
+
|
|
128
|
+
Features:
|
|
129
|
+
- 类型安全配置(Pydantic 验证)
|
|
130
|
+
- 多层优先级系统
|
|
131
|
+
- 配置冲突检测
|
|
132
|
+
- 文件热重载(基于 mtime 缓存)
|
|
133
|
+
- 原子文件写入
|
|
134
|
+
- 环境变量同步(reload 模式)
|
|
56
135
|
"""
|
|
57
|
-
config_path = get_config_path()
|
|
58
136
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
_instance: Optional["UnifiedConfigManager"] = None
|
|
138
|
+
_config_path: Path = Path.home() / ".config" / "autoglm" / "config.json"
|
|
62
139
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
140
|
+
def __new__(cls):
|
|
141
|
+
"""单例模式."""
|
|
142
|
+
if cls._instance is None:
|
|
143
|
+
cls._instance = super().__new__(cls)
|
|
144
|
+
cls._instance._initialized = False
|
|
145
|
+
return cls._instance
|
|
67
146
|
|
|
68
|
-
|
|
69
|
-
|
|
147
|
+
def __init__(self):
|
|
148
|
+
"""初始化配置管理器."""
|
|
149
|
+
if hasattr(self, "_initialized") and self._initialized:
|
|
150
|
+
return
|
|
70
151
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
152
|
+
# 四层配置
|
|
153
|
+
self._cli_layer = ConfigLayer(source=ConfigSource.CLI)
|
|
154
|
+
self._env_layer = ConfigLayer(source=ConfigSource.ENV)
|
|
155
|
+
self._file_layer = ConfigLayer(source=ConfigSource.FILE)
|
|
156
|
+
self._default_layer = ConfigLayer(
|
|
157
|
+
base_url="",
|
|
158
|
+
model_name="autoglm-phone-9b",
|
|
159
|
+
api_key="EMPTY",
|
|
160
|
+
source=ConfigSource.DEFAULT,
|
|
161
|
+
)
|
|
76
162
|
|
|
163
|
+
# 文件缓存(带修改时间戳)
|
|
164
|
+
self._file_cache: Optional[dict] = None
|
|
165
|
+
self._file_mtime: Optional[float] = None
|
|
77
166
|
|
|
78
|
-
|
|
79
|
-
|
|
167
|
+
# 有效配置缓存
|
|
168
|
+
self._effective_config: Optional[ConfigModel] = None
|
|
80
169
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"""
|
|
84
|
-
config_path = get_config_path()
|
|
170
|
+
self._initialized = True
|
|
171
|
+
logger.debug("UnifiedConfigManager initialized")
|
|
85
172
|
|
|
86
|
-
|
|
87
|
-
logger.debug(f"Config file does not exist: {config_path}")
|
|
88
|
-
return True
|
|
173
|
+
# ==================== 配置加载 ====================
|
|
89
174
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
175
|
+
def set_cli_config(
|
|
176
|
+
self,
|
|
177
|
+
base_url: Optional[str] = None,
|
|
178
|
+
model_name: Optional[str] = None,
|
|
179
|
+
api_key: Optional[str] = None,
|
|
180
|
+
) -> None:
|
|
181
|
+
"""
|
|
182
|
+
设置 CLI 参数配置(最高优先级).
|
|
97
183
|
|
|
184
|
+
Args:
|
|
185
|
+
base_url: 从 --base-url 获取的值
|
|
186
|
+
model_name: 从 --model 获取的值
|
|
187
|
+
api_key: 从 --apikey 获取的值
|
|
188
|
+
"""
|
|
189
|
+
self._cli_layer = ConfigLayer(
|
|
190
|
+
base_url=base_url,
|
|
191
|
+
model_name=model_name,
|
|
192
|
+
api_key=api_key,
|
|
193
|
+
source=ConfigSource.CLI,
|
|
194
|
+
)
|
|
195
|
+
self._effective_config = None # 清除缓存
|
|
196
|
+
logger.debug(f"CLI config set: {self._cli_layer.to_dict()}")
|
|
98
197
|
|
|
99
|
-
def
|
|
100
|
-
|
|
198
|
+
def load_env_config(self) -> None:
|
|
199
|
+
"""
|
|
200
|
+
从环境变量加载配置.
|
|
101
201
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
202
|
+
读取环境变量:
|
|
203
|
+
- AUTOGLM_BASE_URL
|
|
204
|
+
- AUTOGLM_MODEL_NAME
|
|
205
|
+
- AUTOGLM_API_KEY
|
|
206
|
+
"""
|
|
207
|
+
base_url = os.getenv("AUTOGLM_BASE_URL")
|
|
208
|
+
model_name = os.getenv("AUTOGLM_MODEL_NAME")
|
|
209
|
+
api_key = os.getenv("AUTOGLM_API_KEY")
|
|
105
210
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
211
|
+
self._env_layer = ConfigLayer(
|
|
212
|
+
base_url=base_url if base_url else None,
|
|
213
|
+
model_name=model_name if model_name else None,
|
|
214
|
+
api_key=api_key if api_key else None,
|
|
215
|
+
source=ConfigSource.ENV,
|
|
216
|
+
)
|
|
217
|
+
self._effective_config = None # 清除缓存
|
|
218
|
+
logger.debug(f"Environment config loaded: {self._env_layer.to_dict()}")
|
|
219
|
+
|
|
220
|
+
def load_file_config(self, force_reload: bool = False) -> bool:
|
|
221
|
+
"""
|
|
222
|
+
从文件加载配置,支持热重载.
|
|
223
|
+
|
|
224
|
+
基于文件修改时间(mtime)的缓存机制:
|
|
225
|
+
- 如果文件未变化且有缓存,直接使用缓存
|
|
226
|
+
- 否则重新读取文件
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
force_reload: 强制重新加载,即使文件未变化
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
bool: 如果配置被加载/重载返回 True,否则返回 False
|
|
233
|
+
"""
|
|
234
|
+
if not self._config_path.exists():
|
|
235
|
+
logger.debug(f"Config file not found: {self._config_path}")
|
|
236
|
+
self._file_layer = ConfigLayer(source=ConfigSource.FILE)
|
|
237
|
+
self._file_cache = None
|
|
238
|
+
self._file_mtime = None
|
|
239
|
+
self._effective_config = None
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
# 获取文件修改时间
|
|
244
|
+
current_mtime = self._config_path.stat().st_mtime
|
|
245
|
+
|
|
246
|
+
# 使用缓存(如果文件未变化)
|
|
247
|
+
if (
|
|
248
|
+
not force_reload
|
|
249
|
+
and self._file_mtime == current_mtime
|
|
250
|
+
and self._file_cache
|
|
251
|
+
):
|
|
252
|
+
logger.debug("Using cached config file (file unchanged)")
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
# 读取并解析文件
|
|
256
|
+
with open(self._config_path, "r", encoding="utf-8") as f:
|
|
257
|
+
config_data = json.load(f)
|
|
258
|
+
|
|
259
|
+
# 更新缓存
|
|
260
|
+
self._file_cache = config_data
|
|
261
|
+
self._file_mtime = current_mtime
|
|
262
|
+
|
|
263
|
+
# 更新文件层
|
|
264
|
+
self._file_layer = ConfigLayer(
|
|
265
|
+
base_url=config_data.get("base_url"),
|
|
266
|
+
model_name=config_data.get("model_name"),
|
|
267
|
+
api_key=config_data.get("api_key"),
|
|
268
|
+
source=ConfigSource.FILE,
|
|
269
|
+
)
|
|
270
|
+
self._effective_config = None # 清除缓存
|
|
271
|
+
|
|
272
|
+
logger.info(f"Config file loaded from {self._config_path}")
|
|
273
|
+
return True
|
|
274
|
+
|
|
275
|
+
except json.JSONDecodeError as e:
|
|
276
|
+
logger.warning(f"Failed to parse config file: {e}")
|
|
277
|
+
self._file_layer = ConfigLayer(source=ConfigSource.FILE)
|
|
278
|
+
self._file_cache = None
|
|
279
|
+
self._file_mtime = None
|
|
280
|
+
self._effective_config = None
|
|
281
|
+
return False
|
|
282
|
+
except Exception as e:
|
|
283
|
+
logger.error(f"Failed to read config file: {e}")
|
|
284
|
+
self._file_layer = ConfigLayer(source=ConfigSource.FILE)
|
|
285
|
+
self._file_cache = None
|
|
286
|
+
self._file_mtime = None
|
|
287
|
+
self._effective_config = None
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
def save_file_config(
|
|
291
|
+
self,
|
|
292
|
+
base_url: str,
|
|
293
|
+
model_name: str,
|
|
294
|
+
api_key: Optional[str] = None,
|
|
295
|
+
merge_mode: bool = True,
|
|
296
|
+
) -> bool:
|
|
297
|
+
"""
|
|
298
|
+
保存配置到文件,支持合并模式.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
base_url: Base URL
|
|
302
|
+
model_name: 模型名称
|
|
303
|
+
api_key: API key(可选)
|
|
304
|
+
merge_mode: 是否合并现有配置(True: 保留未提供的字段)
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
bool: 成功返回 True,失败返回 False
|
|
308
|
+
"""
|
|
309
|
+
try:
|
|
310
|
+
# 确保目录存在
|
|
311
|
+
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
312
|
+
|
|
313
|
+
# 准备新配置
|
|
314
|
+
new_config = {
|
|
315
|
+
"base_url": base_url,
|
|
316
|
+
"model_name": model_name,
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if api_key:
|
|
320
|
+
new_config["api_key"] = api_key
|
|
321
|
+
|
|
322
|
+
# 合并模式:保留现有文件中未提供的字段
|
|
323
|
+
if merge_mode and self._config_path.exists():
|
|
324
|
+
try:
|
|
325
|
+
with open(self._config_path, "r", encoding="utf-8") as f:
|
|
326
|
+
existing = json.load(f)
|
|
327
|
+
|
|
328
|
+
# 保留 api_key(如果新配置未提供)
|
|
329
|
+
if not api_key and "api_key" in existing:
|
|
330
|
+
new_config["api_key"] = existing["api_key"]
|
|
331
|
+
|
|
332
|
+
except (json.JSONDecodeError, Exception) as e:
|
|
333
|
+
logger.warning(f"Could not merge with existing config: {e}")
|
|
334
|
+
|
|
335
|
+
# 原子写入:临时文件 + 重命名
|
|
336
|
+
temp_path = self._config_path.with_suffix(".tmp")
|
|
337
|
+
with open(temp_path, "w", encoding="utf-8") as f:
|
|
338
|
+
json.dump(new_config, f, indent=2, ensure_ascii=False)
|
|
339
|
+
|
|
340
|
+
temp_path.replace(self._config_path)
|
|
341
|
+
|
|
342
|
+
logger.info(f"Configuration saved to {self._config_path}")
|
|
343
|
+
|
|
344
|
+
# 重新加载文件配置以更新缓存
|
|
345
|
+
self.load_file_config(force_reload=True)
|
|
346
|
+
|
|
347
|
+
return True
|
|
348
|
+
|
|
349
|
+
except Exception as e:
|
|
350
|
+
logger.error(f"Failed to save config: {e}")
|
|
351
|
+
return False
|
|
352
|
+
|
|
353
|
+
def delete_file_config(self) -> bool:
|
|
354
|
+
"""
|
|
355
|
+
删除配置文件.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
bool: 成功或文件不存在返回 True,失败返回 False
|
|
359
|
+
"""
|
|
360
|
+
if not self._config_path.exists():
|
|
361
|
+
logger.debug("Config file doesn't exist, nothing to delete")
|
|
362
|
+
return True
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
self._config_path.unlink()
|
|
366
|
+
self._file_cache = None
|
|
367
|
+
self._file_mtime = None
|
|
368
|
+
self._file_layer = ConfigLayer(source=ConfigSource.FILE)
|
|
369
|
+
self._effective_config = None
|
|
370
|
+
logger.info(f"Configuration deleted: {self._config_path}")
|
|
371
|
+
return True
|
|
372
|
+
except Exception as e:
|
|
373
|
+
logger.error(f"Failed to delete config file: {e}")
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
# ==================== 配置合并 ====================
|
|
377
|
+
|
|
378
|
+
def get_effective_config(self, reload_file: bool = False) -> ConfigModel:
|
|
379
|
+
"""
|
|
380
|
+
获取合并后的有效配置.
|
|
381
|
+
|
|
382
|
+
配置优先级:CLI > ENV > FILE > DEFAULT
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
reload_file: 是否强制重新加载配置文件
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
ConfigModel: 验证后的配置对象
|
|
389
|
+
"""
|
|
390
|
+
# 重新加载文件(热重载支持)
|
|
391
|
+
if reload_file:
|
|
392
|
+
self.load_file_config(force_reload=True)
|
|
393
|
+
|
|
394
|
+
# 返回缓存(如果可用)
|
|
395
|
+
if self._effective_config is not None:
|
|
396
|
+
return self._effective_config
|
|
397
|
+
|
|
398
|
+
# 按优先级合并配置
|
|
399
|
+
merged = {}
|
|
400
|
+
|
|
401
|
+
for key in ["base_url", "model_name", "api_key"]:
|
|
402
|
+
# 1. CLI 优先
|
|
403
|
+
if self._cli_layer.has_value(key):
|
|
404
|
+
merged[key] = getattr(self._cli_layer, key)
|
|
405
|
+
# 2. 环境变量
|
|
406
|
+
elif self._env_layer.has_value(key):
|
|
407
|
+
merged[key] = getattr(self._env_layer, key)
|
|
408
|
+
# 3. 配置文件
|
|
409
|
+
elif self._file_layer.has_value(key):
|
|
410
|
+
merged[key] = getattr(self._file_layer, key)
|
|
411
|
+
# 4. 默认值
|
|
412
|
+
else:
|
|
413
|
+
merged[key] = getattr(self._default_layer, key)
|
|
414
|
+
|
|
415
|
+
# 验证并缓存
|
|
416
|
+
try:
|
|
417
|
+
self._effective_config = ConfigModel(**merged)
|
|
418
|
+
logger.debug(f"Effective config computed: {merged}")
|
|
419
|
+
return self._effective_config
|
|
420
|
+
except Exception as e:
|
|
421
|
+
logger.error(f"Configuration validation failed: {e}")
|
|
422
|
+
# 降级到默认值
|
|
423
|
+
self._effective_config = ConfigModel()
|
|
424
|
+
return self._effective_config
|
|
425
|
+
|
|
426
|
+
def get_config_source(self) -> ConfigSource:
|
|
427
|
+
"""
|
|
428
|
+
获取主要配置来源.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
ConfigSource: 最高优先级的非空配置源
|
|
432
|
+
"""
|
|
433
|
+
# 检查 CLI 是否有值
|
|
434
|
+
if self._cli_layer.to_dict():
|
|
435
|
+
return ConfigSource.CLI
|
|
436
|
+
|
|
437
|
+
# 检查 ENV 是否有值
|
|
438
|
+
if self._env_layer.to_dict():
|
|
439
|
+
return ConfigSource.ENV
|
|
440
|
+
|
|
441
|
+
# 检查 FILE 是否有值
|
|
442
|
+
if self._file_layer.to_dict():
|
|
443
|
+
return ConfigSource.FILE
|
|
444
|
+
|
|
445
|
+
return ConfigSource.DEFAULT
|
|
446
|
+
|
|
447
|
+
def get_field_source(self, field: str) -> ConfigSource:
|
|
448
|
+
"""
|
|
449
|
+
获取特定字段的配置来源.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
field: 字段名('base_url', 'model_name', 'api_key')
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
ConfigSource: 该字段的配置来源
|
|
456
|
+
"""
|
|
457
|
+
if self._cli_layer.has_value(field):
|
|
458
|
+
return ConfigSource.CLI
|
|
459
|
+
elif self._env_layer.has_value(field):
|
|
460
|
+
return ConfigSource.ENV
|
|
461
|
+
elif self._file_layer.has_value(field):
|
|
462
|
+
return ConfigSource.FILE
|
|
463
|
+
else:
|
|
464
|
+
return ConfigSource.DEFAULT
|
|
465
|
+
|
|
466
|
+
# ==================== 配置冲突检测 ====================
|
|
467
|
+
|
|
468
|
+
def detect_conflicts(self) -> list[ConfigConflict]:
|
|
469
|
+
"""
|
|
470
|
+
检测配置冲突.
|
|
471
|
+
|
|
472
|
+
冲突定义:
|
|
473
|
+
1. 配置文件中有某个字段的值
|
|
474
|
+
2. CLI 或 ENV 有该字段的不同值(覆盖)
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
list[ConfigConflict]: 冲突列表
|
|
478
|
+
"""
|
|
479
|
+
conflicts = []
|
|
480
|
+
|
|
481
|
+
if not self._file_layer.to_dict():
|
|
482
|
+
return conflicts # 无文件配置,无冲突
|
|
483
|
+
|
|
484
|
+
for key in ["base_url", "model_name", "api_key"]:
|
|
485
|
+
file_value = getattr(self._file_layer, key, None)
|
|
486
|
+
|
|
487
|
+
if file_value is None:
|
|
488
|
+
continue # 文件中没有此字段
|
|
489
|
+
|
|
490
|
+
# 检查 CLI 覆盖
|
|
491
|
+
cli_value = getattr(self._cli_layer, key, None)
|
|
492
|
+
if cli_value is not None and cli_value != file_value:
|
|
493
|
+
conflicts.append(
|
|
494
|
+
ConfigConflict(
|
|
495
|
+
field=key,
|
|
496
|
+
file_value=file_value,
|
|
497
|
+
override_value=cli_value,
|
|
498
|
+
override_source=ConfigSource.CLI,
|
|
499
|
+
)
|
|
500
|
+
)
|
|
501
|
+
continue
|
|
502
|
+
|
|
503
|
+
# 检查 ENV 覆盖
|
|
504
|
+
env_value = getattr(self._env_layer, key, None)
|
|
505
|
+
if env_value is not None and env_value != file_value:
|
|
506
|
+
conflicts.append(
|
|
507
|
+
ConfigConflict(
|
|
508
|
+
field=key,
|
|
509
|
+
file_value=file_value,
|
|
510
|
+
override_value=env_value,
|
|
511
|
+
override_source=ConfigSource.ENV,
|
|
512
|
+
)
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
return conflicts
|
|
516
|
+
|
|
517
|
+
# ==================== 环境变量同步 ====================
|
|
518
|
+
|
|
519
|
+
def sync_to_env(self) -> None:
|
|
520
|
+
"""
|
|
521
|
+
将有效配置同步到环境变量.
|
|
522
|
+
|
|
523
|
+
这是 --reload 模式必需的:
|
|
524
|
+
- uvicorn reload 会启动新进程
|
|
525
|
+
- 新进程继承父进程的环境变量
|
|
526
|
+
- 通过环境变量恢复配置
|
|
527
|
+
"""
|
|
528
|
+
config = self.get_effective_config()
|
|
529
|
+
|
|
530
|
+
os.environ["AUTOGLM_BASE_URL"] = config.base_url
|
|
531
|
+
os.environ["AUTOGLM_MODEL_NAME"] = config.model_name
|
|
532
|
+
os.environ["AUTOGLM_API_KEY"] = config.api_key
|
|
533
|
+
|
|
534
|
+
logger.debug("Configuration synced to environment variables")
|
|
535
|
+
|
|
536
|
+
# ==================== 工具方法 ====================
|
|
537
|
+
|
|
538
|
+
def get_config_path(self) -> Path:
|
|
539
|
+
"""获取配置文件路径.
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
Path: 配置文件路径
|
|
543
|
+
"""
|
|
544
|
+
return self._config_path
|
|
545
|
+
|
|
546
|
+
def to_dict(self) -> dict:
|
|
547
|
+
"""
|
|
548
|
+
将有效配置转换为字典.
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
dict: 配置字典
|
|
552
|
+
"""
|
|
553
|
+
config = self.get_effective_config()
|
|
554
|
+
return {
|
|
555
|
+
"base_url": config.base_url,
|
|
556
|
+
"model_name": config.model_name,
|
|
557
|
+
"api_key": config.api_key,
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
# ==================== 全局单例 ====================
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
# 全局配置管理器单例
|
|
565
|
+
config_manager = UnifiedConfigManager()
|
AutoGLM_GUI/platform_utils.py
CHANGED
|
@@ -11,6 +11,25 @@ def is_windows() -> bool:
|
|
|
11
11
|
return platform.system() == "Windows"
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def run_cmd_silently_sync(
|
|
15
|
+
cmd: Sequence[str], timeout: float | None = None
|
|
16
|
+
) -> subprocess.CompletedProcess:
|
|
17
|
+
"""Run a command synchronously, suppressing output but preserving it in the result.
|
|
18
|
+
|
|
19
|
+
This is the synchronous version that works on all platforms.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
cmd: Command to run as a sequence of strings
|
|
23
|
+
timeout: Optional timeout in seconds
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
CompletedProcess with stdout/stderr captured
|
|
27
|
+
"""
|
|
28
|
+
return subprocess.run(
|
|
29
|
+
cmd, capture_output=True, text=True, check=False, timeout=timeout
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
14
33
|
async def run_cmd_silently(cmd: Sequence[str]) -> subprocess.CompletedProcess:
|
|
15
34
|
"""Run a command, suppressing output but preserving it in the result; safe for async contexts on all platforms."""
|
|
16
35
|
if is_windows():
|