autoglm-gui 0.4.11__py3-none-any.whl → 0.4.12__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 (37) hide show
  1. AutoGLM_GUI/__init__.py +8 -0
  2. AutoGLM_GUI/__main__.py +29 -34
  3. AutoGLM_GUI/adb_plus/__init__.py +6 -0
  4. AutoGLM_GUI/adb_plus/device.py +50 -0
  5. AutoGLM_GUI/adb_plus/ip.py +78 -0
  6. AutoGLM_GUI/adb_plus/serial.py +35 -0
  7. AutoGLM_GUI/api/__init__.py +8 -0
  8. AutoGLM_GUI/api/agents.py +76 -67
  9. AutoGLM_GUI/api/devices.py +96 -6
  10. AutoGLM_GUI/api/media.py +12 -235
  11. AutoGLM_GUI/config_manager.py +538 -97
  12. AutoGLM_GUI/exceptions.py +7 -0
  13. AutoGLM_GUI/platform_utils.py +19 -0
  14. AutoGLM_GUI/schemas.py +35 -2
  15. AutoGLM_GUI/scrcpy_protocol.py +46 -0
  16. AutoGLM_GUI/scrcpy_stream.py +192 -307
  17. AutoGLM_GUI/server.py +7 -2
  18. AutoGLM_GUI/socketio_server.py +125 -0
  19. AutoGLM_GUI/static/assets/{about-wSo3UgQ-.js → about-kgOkkOWe.js} +1 -1
  20. AutoGLM_GUI/static/assets/chat-CZV3RByK.js +149 -0
  21. AutoGLM_GUI/static/assets/{index-B5u1xtK1.js → index-BPYHsweG.js} +1 -1
  22. AutoGLM_GUI/static/assets/index-Beu9cbSy.css +1 -0
  23. AutoGLM_GUI/static/assets/index-DfI_Z1Cx.js +10 -0
  24. AutoGLM_GUI/static/assets/worker-D6BRitjy.js +1 -0
  25. AutoGLM_GUI/static/index.html +2 -2
  26. {autoglm_gui-0.4.11.dist-info → autoglm_gui-0.4.12.dist-info}/METADATA +2 -1
  27. autoglm_gui-0.4.12.dist-info/RECORD +56 -0
  28. AutoGLM_GUI/resources/apks/ADBKeyBoard.LICENSE.txt +0 -339
  29. AutoGLM_GUI/resources/apks/ADBKeyBoard.README.txt +0 -1
  30. AutoGLM_GUI/resources/apks/ADBKeyboard.apk +0 -0
  31. AutoGLM_GUI/static/assets/chat-BcY2K0yj.js +0 -25
  32. AutoGLM_GUI/static/assets/index-CHrYo3Qj.css +0 -1
  33. AutoGLM_GUI/static/assets/index-D5BALRbT.js +0 -10
  34. autoglm_gui-0.4.11.dist-info/RECORD +0 -52
  35. {autoglm_gui-0.4.11.dist-info → autoglm_gui-0.4.12.dist-info}/WHEEL +0 -0
  36. {autoglm_gui-0.4.11.dist-info → autoglm_gui-0.4.12.dist-info}/entry_points.txt +0 -0
  37. {autoglm_gui-0.4.11.dist-info → autoglm_gui-0.4.12.dist-info}/licenses/LICENSE +0 -0
@@ -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
- Returns:
16
- Path: 配置文件路径 (~/.config/autoglm/config.json)
17
- """
18
- config_dir = Path.home() / ".config" / "autoglm"
19
- return config_dir / "config.json"
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
- Returns:
26
- dict | None: 配置字典,如果文件不存在或加载失败则返回 None
119
+ # ==================== 统一配置管理器 ====================
120
+
121
+
122
+ class UnifiedConfigManager:
27
123
  """
28
- config_path = get_config_path()
29
-
30
- # 文件不存在(首次运行)
31
- if not config_path.exists():
32
- logger.debug(f"Config file not found at {config_path}")
33
- return None
34
-
35
- try:
36
- with open(config_path, "r", encoding="utf-8") as f:
37
- config = json.load(f)
38
- logger.info(f"Loaded configuration from {config_path}")
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
- try:
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
- temp_path = config_path.with_suffix(".tmp")
65
- with open(temp_path, "w", encoding="utf-8") as f:
66
- json.dump(config, f, indent=2, ensure_ascii=False)
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
- temp_path.replace(config_path)
147
+ def __init__(self):
148
+ """初始化配置管理器."""
149
+ if hasattr(self, "_initialized") and self._initialized:
150
+ return
70
151
 
71
- logger.info(f"Configuration saved to {config_path}")
72
- return True
73
- except Exception as e:
74
- logger.error(f"Failed to save config file {config_path}: {e}")
75
- return False
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
- def delete_config_file() -> bool:
79
- """删除配置文件.
167
+ # 有效配置缓存
168
+ self._effective_config: Optional[ConfigModel] = None
80
169
 
81
- Returns:
82
- bool: 成功返回 True,失败返回 False
83
- """
84
- config_path = get_config_path()
170
+ self._initialized = True
171
+ logger.debug("UnifiedConfigManager initialized")
85
172
 
86
- if not config_path.exists():
87
- logger.debug(f"Config file does not exist: {config_path}")
88
- return True
173
+ # ==================== 配置加载 ====================
89
174
 
90
- try:
91
- config_path.unlink()
92
- logger.info(f"Configuration deleted: {config_path}")
93
- return True
94
- except Exception as e:
95
- logger.error(f"Failed to delete config file {config_path}: {e}")
96
- return False
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 merge_configs(file_config: dict | None, cli_config: dict | None) -> dict:
100
- """合并配置(优先级:CLI > 文件 > 默认值).
198
+ def load_env_config(self) -> None:
199
+ """
200
+ 从环境变量加载配置.
101
201
 
102
- Args:
103
- file_config: 从文件加载的配置(可为 None)
104
- cli_config: CLI 参数配置(可为 None)
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
- Returns:
107
- dict: 合并后的配置字典
108
- """
109
- # 从默认配置开始
110
- merged = DEFAULT_CONFIG.copy()
111
-
112
- # 应用文件配置(如果存在)
113
- if file_config:
114
- for key in DEFAULT_CONFIG.keys():
115
- if key in file_config:
116
- merged[key] = file_config[key]
117
-
118
- # 应用 CLI 配置(最高优先级)
119
- if cli_config:
120
- for key in DEFAULT_CONFIG.keys():
121
- if key in cli_config:
122
- merged[key] = cli_config[key]
123
-
124
- return merged
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()
@@ -0,0 +1,7 @@
1
+ """Custom exceptions for AutoGLM-GUI."""
2
+
3
+
4
+ class DeviceNotAvailableError(Exception):
5
+ """Raised when device is not available (disconnected/offline)."""
6
+
7
+ pass
@@ -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():