Add HeXi bot codebase: custom plugins, web frontends, tests
- hexi core: message handling, rate limiting, cooldown, plugin manager - Custom plugins: BF stats, daily check-in, quotes, persona cards, etc. - Community plugins vendored under hexi/plugins with local fixes - Web admin frontends (learning-chat, persona-admin), unified hexi/web - Tests for rate_limit/cooldown/memes/persona; poetry.lock Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user