"""NoneBot 版插件配置加载。 配置持久化在数据目录 config.json,键值结构与原 AstrBot 配置分组一致, 这样可以直接喂给 core 的 ConfigManager(它本质只读嵌套 dict)。 """ from __future__ import annotations import json import os from pathlib import Path from .core.shared.constants import PLUGIN_NAME from .core.utils.paths import get_data_dir class ConfigDict(dict): """兼容原 ConfigManager 的 dict;save_config 会把改动持久化到 config.json。""" def save_config(self) -> None: try: config_file().write_text( json.dumps(dict(self), ensure_ascii=False, indent=2, default=str), encoding="utf-8", ) except OSError: pass def _build_from_items(items: dict) -> dict: """从 schema 的 items 递归生成嵌套默认配置。""" out: dict = {} for key, item in items.items(): if not isinstance(item, dict): continue if item.get("type") == "object" and isinstance(item.get("items"), dict): out[key] = _build_from_items(item["items"]) elif "default" in item: out[key] = item["default"] return out def _schema_defaults() -> dict: """从插件的 _conf_schema.json 读取完整默认配置。""" schema_path = Path(__file__).resolve().parents[0] / "_conf_schema.json" try: schema = json.loads(schema_path.read_text(encoding="utf-8-sig")) except (OSError, json.JSONDecodeError): return {} defaults: dict = {} for group, spec in schema.items(): if isinstance(spec, dict) and isinstance(spec.get("items"), dict): defaults[group] = _build_from_items(spec["items"]) return defaults def _read_config_value(*names: str, default: str = "") -> str: """依次从 shell 环境变量与 NoneBot driver.config(.env) 读取值。 NoneBot 的 .env 是通过 pydantic 加载的,不会写入 os.environ, 因此必须能从 get_driver().config 的小写属性读取。优先环境变量。 """ for n in names: v = os.environ.get(n) if v: return str(v) try: from nonebot import get_driver drv_cfg = get_driver().config except Exception: return default for n in names: attr = n.lower() try: v = getattr(drv_cfg, attr, None) except Exception: v = None if v: return str(v) return default def _llm_env_values() -> dict: """用户必填的 LLM 端点/密钥/模型;AstrBot 版是 Provider 体系,插件本身不存这些。""" return { "llm_api_base": _read_config_value("HEXI_LLM_API_BASE"), "llm_api_key": _read_config_value("HEXI_LLM_API_KEY"), "llm_model": _read_config_value("HEXI_LLM_MODEL"), } def _default_config() -> dict: cfg = _schema_defaults() cfg.setdefault("llm", {}).update(_llm_env_values()) cfg.setdefault("basic", {}).setdefault("output_format", ["image"]) cfg.setdefault("basic", {}).setdefault("enable_base64_image", True) cfg.setdefault("basic", {}).setdefault("analysis_days", 1) cfg.setdefault("basic", {}).setdefault("max_messages", 1000) cfg.setdefault("basic", {}).setdefault("min_messages_threshold", 50) cfg.setdefault("basic", {}).setdefault("filter_bot_messages", True) # 优先读本地消息库(learning_chat 落库),取不到再回退接口分页 cfg.setdefault("basic", {}).setdefault("use_local_history", True) cfg.setdefault("analysis_features", {}).setdefault("chat_quality_analysis_enabled", False) cfg.setdefault("incremental", {}).setdefault("incremental_enabled", False) return cfg def config_file() -> Path: return get_data_dir(PLUGIN_NAME) / "config.json" def load_config() -> ConfigDict: cfg = ConfigDict(_default_config()) f = config_file() if f.exists(): try: user = json.loads(f.read_text(encoding="utf-8")) for group, values in user.items(): if isinstance(values, dict): cfg.setdefault(group, {}).update(values) else: cfg[group] = values except (OSError, json.JSONDecodeError) as e: _log_warn(f"读取配置文件失败: {e}") # 环境变量/NoneBot .env 永远优先于持久化 config.json,避免被空值覆盖 cfg.setdefault("llm", {}).update(_llm_env_values()) return cfg def save_config(cfg: dict) -> None: try: config_file().write_text( json.dumps(cfg, ensure_ascii=False, indent=2), encoding="utf-8" ) except OSError as e: _log_warn(f"保存配置文件失败: {e}") def _log_warn(msg: str) -> None: try: from .core.utils.logger import logger logger.warning(msg) except Exception: pass