Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

130 lines
5.0 KiB
Python

import os
from pathlib import Path
import tempfile
from threading import RLock
from typing import Any, Dict, List, Optional
import yaml
from ..services.config.loader import get_runtime_settings_dir
class ConfigManager:
"""
Minimal runtime YAML manager for `data/user/settings/main.yaml`.
The long-lived configuration model is now:
- `data/user/settings/model_catalog.json` for providers and credentials
- `data/user/settings/*.json` / `*.yaml` for runtime behavior
"""
_instance: Optional["ConfigManager"] = None
_lock = RLock()
def __new__(cls, project_root: Optional[Path] = None):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super(ConfigManager, cls).__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self, project_root: Optional[Path] = None):
if getattr(self, "_initialized", False):
return
self.project_root = project_root or Path(__file__).parent.parent.parent
self.config_path = get_runtime_settings_dir(self.project_root) / "main.yaml"
self._config_cache: Dict[str, Any] = {}
self._last_mtime: float = 0.0
self._initialized = True
def _read_yaml(self) -> Dict[str, Any]:
if not self.config_path.exists():
return {}
with open(self.config_path, "r", encoding="utf-8") as handle:
return yaml.safe_load(handle) or {}
def _deep_update(self, target: Dict[str, Any], source: Dict[str, Any]) -> None:
for key, value in source.items():
if isinstance(value, dict) and isinstance(target.get(key), dict):
self._deep_update(target[key], value)
else:
target[key] = value
def load_config(self, force_reload: bool = False) -> Dict[str, Any]:
with self._lock:
if not self.config_path.exists():
self._config_cache = {}
self._last_mtime = 0
return {}
current_mtime = self.config_path.stat().st_mtime
if not self._config_cache or force_reload or current_mtime > self._last_mtime:
self._config_cache = self._read_yaml()
self._last_mtime = current_mtime
return yaml.safe_load(yaml.safe_dump(self._config_cache, sort_keys=False)) or {}
def save_config(self, config: Dict[str, Any]) -> bool:
with self._lock:
current = self.load_config(force_reload=True)
self._deep_update(current, config)
self.config_path.parent.mkdir(parents=True, exist_ok=True)
yaml_str = yaml.safe_dump(
current,
default_flow_style=False,
allow_unicode=True,
sort_keys=False,
)
fd, tmp_path = tempfile.mkstemp(prefix="main.yaml.", dir=str(self.config_path.parent))
try:
with os.fdopen(fd, "w", encoding="utf-8") as tmp:
tmp.write(yaml_str)
tmp.flush()
os.fsync(tmp.fileno())
os.replace(tmp_path, self.config_path)
self._config_cache = current
self._last_mtime = self.config_path.stat().st_mtime
return True
finally:
if os.path.exists(tmp_path):
try:
os.remove(tmp_path)
except OSError:
pass
def get_env_info(self) -> Dict[str, str]:
return {"model": self._runtime_key_values().get("LLM_MODEL", "")}
def validate_required_env(self, keys: List[str]) -> Dict[str, List[str]]:
values = self._runtime_key_values()
missing = [key for key in keys if not values.get(key)]
return {"missing": missing}
def _runtime_key_values(self) -> Dict[str, str]:
from deeptutor.services.config.model_catalog import ModelCatalogService
from deeptutor.services.config.runtime_settings import RuntimeSettingsService
settings_dir = get_runtime_settings_dir(self.project_root)
catalog_service = ModelCatalogService(settings_dir / "model_catalog.json")
catalog = catalog_service.load()
llm_profile = catalog_service.get_active_profile(catalog, "llm") or {}
llm_model = catalog_service.get_active_model(catalog, "llm") or {}
system = RuntimeSettingsService.get_instance(settings_dir).load_system()
return {
"BACKEND_PORT": str(system["backend_port"]),
"FRONTEND_PORT": str(system["frontend_port"]),
"LLM_BINDING": str(llm_profile.get("binding") or ""),
"LLM_MODEL": str(llm_model.get("model") or ""),
"LLM_API_KEY": str(llm_profile.get("api_key") or ""),
"LLM_HOST": str(llm_profile.get("base_url") or ""),
}
@classmethod
def reset_for_tests(cls) -> None:
with cls._lock:
cls._instance = None