- 群分析: 新增 history_store 只读本地消息源(读 learning_chat 落库,含 uninfo 昵称补齐/去重/截断判定),适配器优先读本地库、失败回退 OneBot 分页;分页 锚点字段回退链修复 NapCat 传 message_seq 翻页断裂;新增「本地记录」开关 - core/message_utils: 新增 common_proc_reply 占位图通用回复(引用消息 + 处理中 动图,支持后台任务显式指定 target),群分析/战况/倒放改用 - web_hub + web: 日志页改固定窗口滚动 + 翻页锚定 + 自动换行,SSE 日志轮转发 reset 帧,入口 HTML no-cache,行数统计增量缓存,CPU 改非阻塞采样,登录信息缓存 - 插件内 CLAUDE.md / DESIGN.md 不入库(.gitignore),galgame_card 两份文档取消 跟踪(文件保留在磁盘) Co-Authored-By: Claude Code <noreply@anthropic.com>
145 lines
4.8 KiB
Python
145 lines
4.8 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)
|
||
# 优先读本地消息库(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
|