143 lines
4.6 KiB
Python
143 lines
4.6 KiB
Python
"""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)
|
|||
|
|
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
|