feat: 将LLM配置主存储迁移到MySQL

变更项: 1) 新增 t_llm_config 数据访问层与建表逻辑。 2) Robot 启动时自动初始化并在空库时从 YAML 导入。 3) 后台 system LLM API 改为读写 MySQL。 4) LLMRegistry 改为优先 MySQL 读取并回退 YAML。 5) DashboardServer 挂载 llm_config_db 提供后台访问。
This commit is contained in:
liuwei
2026-04-20 14:51:43 +08:00
parent ef49588485
commit 1446bf5f39
5 changed files with 287 additions and 24 deletions

View File

@@ -1,40 +1,135 @@
from __future__ import annotations
import json
import time
from pathlib import Path
from typing import Any, Dict, Optional
import yaml
from db.connection import DBConnectionManager
class LLMRegistry:
"""从项目根 config.yaml 读取集中式 LLM 后端配置。"""
"""集中式 LLM 配置注册器。
_cache: Dict[str, Any] = {"mtime": None, "data": {}}
读取优先级:
1. 优先读取 MySQLt_llm_config
2. MySQL 不可用或无数据时,回退读取 config.yaml 的 llm 节点。
"""
_cache: Dict[str, Any] = {
# cache_until: 缓存过期时间戳,避免每次调用都打数据库;
# data: 最近一次成功读取并归一化后的 llm 配置对象。
"cache_until": 0.0,
"data": {},
}
@classmethod
def get_root_config_path(cls) -> Path:
return Path(__file__).resolve().parents[2] / "config.yaml"
@classmethod
def load_root_config(cls) -> Dict[str, Any]:
def _load_llm_from_yaml(cls) -> Dict[str, Any]:
"""从 YAML 读取 llm 配置(兜底来源)。"""
path = cls.get_root_config_path()
if not path.exists():
return {}
stat = path.stat()
if cls._cache["mtime"] == stat.st_mtime and cls._cache["data"]:
return cls._cache["data"]
with open(path, "r", encoding="utf-8") as fp:
data = yaml.safe_load(fp) or {}
cls._cache = {"mtime": stat.st_mtime, "data": data}
return data
root = yaml.safe_load(fp) or {}
llm_config = root.get("llm", {}) or {}
if not isinstance(llm_config, dict):
return {}
return llm_config
@staticmethod
def _loads_json(value: Any) -> Dict[str, Any]:
"""把数据库 JSON 字段统一解析为 dict。"""
if isinstance(value, dict):
return value
if isinstance(value, str):
try:
obj = json.loads(value)
return obj if isinstance(obj, dict) else {}
except json.JSONDecodeError:
return {}
return {}
@classmethod
def _load_llm_from_mysql(cls) -> Dict[str, Any]:
"""从 MySQL 读取 llm 配置。
注意:
1. 该函数必须“无副作用失败”,即任何异常都返回空 dict交由上层做 YAML 回退;
2. 不依赖 Robot 实例,直接走 DBConnectionManager 单例,便于在插件调用链路中复用。
"""
try:
db_manager = DBConnectionManager.get_instance()
if not db_manager or not db_manager.mysql_pool:
return {}
conn = db_manager.get_mysql_connection()
try:
with conn.cursor(dictionary=True) as cursor:
cursor.execute(
"""
SELECT default_backend, backends_json, scenes_json
FROM t_llm_config
WHERE id = 1
LIMIT 1
"""
)
row = cursor.fetchone() or {}
finally:
conn.close()
if not row:
return {}
return {
"default_backend": str(row.get("default_backend") or "").strip(),
"backends": cls._loads_json(row.get("backends_json")),
"scenes": cls._loads_json(row.get("scenes_json")),
}
except Exception:
return {}
@classmethod
def _normalize_llm_config(cls, llm_config: Dict[str, Any]) -> Dict[str, Any]:
"""统一规整 llm 配置结构,避免下游出现类型分支。"""
data = llm_config if isinstance(llm_config, dict) else {}
default_backend = str(data.get("default_backend") or "").strip()
backends = data.get("backends", {}) or {}
scenes = data.get("scenes", {}) or {}
if not isinstance(backends, dict):
backends = {}
if not isinstance(scenes, dict):
scenes = {}
return {
"default_backend": default_backend,
"backends": backends,
"scenes": scenes,
}
@classmethod
def get_llm_config(cls) -> Dict[str, Any]:
config = cls.load_root_config()
llm_config = config.get("llm", {}) or {}
return llm_config if isinstance(llm_config, dict) else {}
"""获取运行时 LLM 配置(优先 MySQL失败回退 YAML"""
now = time.time()
if cls._cache.get("cache_until", 0.0) > now and cls._cache.get("data"):
return cls._cache["data"]
llm_config = cls._load_llm_from_mysql()
if not llm_config:
llm_config = cls._load_llm_from_yaml()
normalized = cls._normalize_llm_config(llm_config)
# 轻量缓存 3 秒:兼顾“后台编辑后较快生效”和“降低高频调用的 DB 压力”。
cls._cache = {
"cache_until": now + 3.0,
"data": normalized,
}
return normalized
@classmethod
def get_default_backend(cls) -> str: