添加文件监视器地基模块,重构模型请求模块使用新版本的配置热重载模块,新增watchfiles依赖
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from pathlib import Path
|
||||
from typing import TypeVar
|
||||
from typing import Any, Callable, Mapping, Sequence, TypeVar
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
import asyncio
|
||||
import copy
|
||||
|
||||
import tomlkit
|
||||
@@ -38,6 +38,7 @@ from .config_base import ConfigBase, Field, AttributeData
|
||||
from .config_utils import recursive_parse_item_to_table, output_config_changes, compare_versions
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.config.file_watcher import FileChange, FileWatcher
|
||||
|
||||
"""
|
||||
如果你想要修改配置文件,请递增version的值
|
||||
@@ -126,7 +127,7 @@ class Config(ConfigBase):
|
||||
|
||||
webui: WebUIConfig = Field(default_factory=WebUIConfig)
|
||||
"""WebUI配置类"""
|
||||
|
||||
|
||||
database: DatabaseConfig = Field(default_factory=DatabaseConfig)
|
||||
"""数据库配置类"""
|
||||
|
||||
@@ -176,12 +177,17 @@ class ConfigManager:
|
||||
self.bot_config_path: Path = BOT_CONFIG_PATH
|
||||
self.model_config_path: Path = MODEL_CONFIG_PATH
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
self.global_config: Config | None = None
|
||||
self.model_config: ModelConfig | None = None
|
||||
self._reload_lock: asyncio.Lock = asyncio.Lock()
|
||||
self._reload_callbacks: list[Callable[[], object]] = []
|
||||
self._file_watcher: FileWatcher | None = None
|
||||
|
||||
def initialize(self):
|
||||
logger.info(f"MaiCore当前版本: {MMC_VERSION}")
|
||||
logger.info("正在品鉴配置文件...")
|
||||
self.global_config: Config = self.load_global_config()
|
||||
self.model_config: ModelConfig = self.load_model_config()
|
||||
self.global_config = self.load_global_config()
|
||||
self.model_config = self.load_model_config()
|
||||
logger.info("非常的新鲜,非常的美味!")
|
||||
|
||||
def load_global_config(self) -> Config:
|
||||
@@ -197,11 +203,74 @@ class ConfigManager:
|
||||
return config
|
||||
|
||||
def get_global_config(self) -> Config:
|
||||
if self.global_config is None:
|
||||
raise RuntimeError("global_config 未初始化")
|
||||
return self.global_config
|
||||
|
||||
def get_model_config(self) -> ModelConfig:
|
||||
if self.model_config is None:
|
||||
raise RuntimeError("model_config 未初始化")
|
||||
return self.model_config
|
||||
|
||||
def register_reload_callback(self, callback: Callable[[], object]) -> None:
|
||||
self._reload_callbacks.append(callback)
|
||||
|
||||
async def reload_config(self) -> bool:
|
||||
async with self._reload_lock:
|
||||
try:
|
||||
global_config_new, global_updated = load_config_from_file(
|
||||
Config,
|
||||
self.bot_config_path,
|
||||
CONFIG_VERSION,
|
||||
)
|
||||
model_config_new, model_updated = load_config_from_file(
|
||||
ModelConfig,
|
||||
self.model_config_path,
|
||||
MODEL_CONFIG_VERSION,
|
||||
True,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error(f"配置重载失败: {exc}")
|
||||
return False
|
||||
|
||||
if global_updated or model_updated:
|
||||
logger.warning("检测到配置版本更新,热重载仅更新内存数据")
|
||||
|
||||
self.global_config = global_config_new
|
||||
self.model_config = model_config_new
|
||||
global global_config, model_config
|
||||
global_config = global_config_new
|
||||
model_config = model_config_new
|
||||
logger.info("配置热重载完成")
|
||||
|
||||
for callback in list(self._reload_callbacks):
|
||||
try:
|
||||
result = callback()
|
||||
if asyncio.iscoroutine(result):
|
||||
await result
|
||||
except Exception as exc:
|
||||
logger.warning(f"配置重载回调执行失败: {exc}")
|
||||
return True
|
||||
|
||||
async def start_file_watcher(self) -> None:
|
||||
if self._file_watcher is not None and self._file_watcher.running:
|
||||
return
|
||||
self._file_watcher = FileWatcher(paths=[self.bot_config_path, self.model_config_path])
|
||||
await self._file_watcher.start(self._handle_file_changes)
|
||||
logger.info("配置文件监视器已启动")
|
||||
|
||||
async def stop_file_watcher(self) -> None:
|
||||
if self._file_watcher is None:
|
||||
return
|
||||
await self._file_watcher.stop()
|
||||
self._file_watcher = None
|
||||
|
||||
async def _handle_file_changes(self, changes: Sequence[FileChange]) -> None:
|
||||
if not changes:
|
||||
return
|
||||
logger.info("检测到配置文件变更,触发热重载")
|
||||
await self.reload_config()
|
||||
|
||||
|
||||
def generate_new_config_file(config_class: type[T], config_path: Path, inner_config_version: str) -> None:
|
||||
"""生成新的配置文件
|
||||
@@ -220,7 +289,13 @@ def load_config_from_file(
|
||||
attribute_data = AttributeData()
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config_data = tomlkit.load(f)
|
||||
old_ver: str = config_data["inner"]["version"] # type: ignore
|
||||
inner_table = config_data.get("inner")
|
||||
if not isinstance(inner_table, Mapping):
|
||||
raise TypeError("配置文件缺少 inner 版本信息")
|
||||
inner_version = inner_table.get("version")
|
||||
if not isinstance(inner_version, str):
|
||||
raise TypeError("配置文件 inner.version 类型错误")
|
||||
old_ver: str = inner_version
|
||||
config_data.remove("inner") # 移除 inner 部分,避免干扰后续处理
|
||||
config_data = config_data.unwrap() # 转换为普通字典,方便后续处理
|
||||
# 保留一份“干净”的原始数据副本,避免第一次 from_dict 过程中对 dict 的就地修改
|
||||
@@ -236,8 +311,7 @@ def load_config_from_file(
|
||||
mig = try_migrate_legacy_bot_config_dict(original_data)
|
||||
if mig.migrated:
|
||||
logger.warning(
|
||||
f"检测到旧版配置结构,已尝试自动修复: {mig.reason}。"
|
||||
f"建议稍后检查并保存生成的新配置文件。"
|
||||
f"检测到旧版配置结构,已尝试自动修复: {mig.reason}。建议稍后检查并保存生成的新配置文件。"
|
||||
)
|
||||
migrated_data = mig.data
|
||||
target_config = config_class.from_dict(attribute_data, migrated_data)
|
||||
|
||||
@@ -7,7 +7,7 @@ import tomlkit
|
||||
from .config_base import ConfigBase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .config import AttributeData
|
||||
from .config_base import AttributeData
|
||||
|
||||
|
||||
def recursive_parse_item_to_table(
|
||||
|
||||
68
src/config/file_watcher.py
Normal file
68
src/config/file_watcher.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Awaitable, Callable, Iterable, Sequence
|
||||
|
||||
from watchfiles import Change, awatch
|
||||
|
||||
import asyncio
|
||||
|
||||
from src.common.logger import get_logger
|
||||
|
||||
|
||||
logger = get_logger("file_watcher")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FileChange:
|
||||
change_type: Change
|
||||
path: Path
|
||||
|
||||
|
||||
ChangeCallback = Callable[[Sequence[FileChange]], Awaitable[None]]
|
||||
|
||||
|
||||
class FileWatcher:
|
||||
def __init__(self, paths: Iterable[Path], debounce_ms: int = 600) -> None:
|
||||
self._paths = [path.resolve() for path in paths]
|
||||
self._debounce_ms = debounce_ms
|
||||
self._running = False
|
||||
self._task: asyncio.Task[None] | None = None
|
||||
|
||||
@property
|
||||
def running(self) -> bool:
|
||||
return self._running
|
||||
|
||||
async def start(self, callback: ChangeCallback) -> None:
|
||||
if self._running:
|
||||
return
|
||||
self._running = True
|
||||
self._task = asyncio.create_task(self._run(callback))
|
||||
|
||||
async def stop(self) -> None:
|
||||
if not self._running:
|
||||
return
|
||||
self._running = False
|
||||
if self._task is None:
|
||||
return
|
||||
self._task.cancel()
|
||||
try:
|
||||
await self._task
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
|
||||
async def _run(self, callback: ChangeCallback) -> None:
|
||||
try:
|
||||
async for changes in awatch(*self._paths, debounce=self._debounce_ms):
|
||||
if not self._running:
|
||||
break
|
||||
try:
|
||||
await callback(self._normalize_changes(changes))
|
||||
except Exception as exc:
|
||||
logger.warning(f"文件变更回调执行失败: {exc}")
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
except Exception as exc:
|
||||
logger.error(f"文件监视器运行异常: {exc}")
|
||||
|
||||
def _normalize_changes(self, changes: set[tuple[Change, str]]) -> list[FileChange]:
|
||||
return [FileChange(change_type=change, path=Path(path)) for change, path in changes]
|
||||
Reference in New Issue
Block a user