2026-09-03 00:44:38 +08:00
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""Bot 全局配置的读取 / 写入(SUPERUSERS / NICKNAME / DEBUG / LOG_LEVEL)。
|
|
|
|
|
|
|
|
|
|
|
|
- 读取:优先取 NoneBot driver.config 的当前生效值,退化到 .env / os.environ。
|
|
|
|
|
|
- 写入:落盘到 .env(保留 JSON 列表格式),写 os.environ(即时可读),
|
|
|
|
|
|
并在可能范围内热应用到 driver.config 与 loguru(LOG_LEVEL)。
|
|
|
|
|
|
- 注意:个别插件在 import 时把 SUPERUSERS/NICKNAME 拷贝成了常量,
|
|
|
|
|
|
想让所有插件完全生效建议保存后重启 bot(前端会给出提示)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
import os
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
from nonebot import get_driver, logger
|
|
|
|
|
|
|
2026-09-08 14:25:32 +08:00
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
2026-09-03 00:44:38 +08:00
|
|
|
|
# 仓库根目录的 .env(NoneBot 启动时自动加载)
|
|
|
|
|
|
_ENV_PATH = _REPO_ROOT / ".env"
|
|
|
|
|
|
|
|
|
|
|
|
# 表单键 -> 环境变量名
|
|
|
|
|
|
_KEY_ENV: dict[str, str] = {
|
|
|
|
|
|
"superusers": "SUPERUSERS",
|
|
|
|
|
|
"nickname": "NICKNAME",
|
|
|
|
|
|
"debug": "DEBUG",
|
|
|
|
|
|
"log_level": "LOG_LEVEL",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# LOG_LEVEL 可选项(写入时建议大写)
|
|
|
|
|
|
LOG_LEVEL_OPTIONS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
|
|
|
|
|
2026-09-08 14:25:32 +08:00
|
|
|
|
# 日志清理大小(MB)默认值(未在 .env 配置时)
|
|
|
|
|
|
_LOG_MAX_MB_DEFAULT = 20
|
|
|
|
|
|
|
2026-09-03 00:44:38 +08:00
|
|
|
|
|
|
|
|
|
|
def _env_get(name: str, default: str = "") -> str:
|
|
|
|
|
|
"""os.environ > .env 文件 > default。"""
|
|
|
|
|
|
val = os.getenv(name)
|
|
|
|
|
|
if val:
|
|
|
|
|
|
return val
|
|
|
|
|
|
if _ENV_PATH.exists():
|
|
|
|
|
|
for line in _ENV_PATH.read_text(encoding="utf-8").splitlines():
|
|
|
|
|
|
line = line.strip()
|
|
|
|
|
|
if line.startswith(name + "=") or line.startswith(name + " ="):
|
|
|
|
|
|
return line.split("=", 1)[1].strip().split(" #", maxsplit=1)[0].strip()
|
|
|
|
|
|
return default
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_list(value: str) -> list[str]:
|
|
|
|
|
|
"""把 env 里可能是 JSON 数组或逗号/空白分隔的列表解析成 list[str]。"""
|
|
|
|
|
|
value = (value or "").strip()
|
|
|
|
|
|
if not value:
|
|
|
|
|
|
return []
|
|
|
|
|
|
try:
|
|
|
|
|
|
parsed = json.loads(value)
|
|
|
|
|
|
if isinstance(parsed, list):
|
|
|
|
|
|
return [str(x) for x in parsed]
|
|
|
|
|
|
return [str(parsed)]
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
# 兼容逗号/空格分隔、带引号的形式
|
|
|
|
|
|
return [
|
|
|
|
|
|
part.strip().strip("\"'").strip()
|
|
|
|
|
|
for part in value.replace(" ", ",").split(",")
|
|
|
|
|
|
if part.strip()
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _serialize_list(items: list[str]) -> str:
|
|
|
|
|
|
"""序列化成 .env 使用的 JSON 数组字符串。"""
|
|
|
|
|
|
return json.dumps([str(x) for x in items], ensure_ascii=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_log_level(value: Any) -> str:
|
|
|
|
|
|
"""把 log_level 统一成大写字符串。"""
|
|
|
|
|
|
if isinstance(value, int):
|
|
|
|
|
|
# loguru 数字等级:20=DEBUG 30=INFO 40=WARNING 50=ERROR
|
|
|
|
|
|
mapping = {10: "TRACE", 20: "DEBUG", 30: "INFO", 40: "WARNING", 50: "ERROR", 60: "CRITICAL"}
|
|
|
|
|
|
return mapping.get(value, "INFO")
|
|
|
|
|
|
return str(value).upper()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-08 14:25:32 +08:00
|
|
|
|
def _current_log_max_mb() -> int:
|
|
|
|
|
|
"""当前日志清理大小(MB)。优先级:HEXI_LOG_MAX_MB > HEXI_LOG_MAX_BYTES(换算) > 默认。"""
|
|
|
|
|
|
raw = _env_get("HEXI_LOG_MAX_MB").strip()
|
|
|
|
|
|
if raw:
|
|
|
|
|
|
try:
|
|
|
|
|
|
val = float(raw)
|
|
|
|
|
|
if val > 0:
|
|
|
|
|
|
return int(val)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
raw_bytes = _env_get("HEXI_LOG_MAX_BYTES").strip()
|
|
|
|
|
|
if raw_bytes:
|
|
|
|
|
|
try:
|
|
|
|
|
|
b = int(raw_bytes)
|
|
|
|
|
|
if b > 0:
|
|
|
|
|
|
return max(1, b // (1024 * 1024))
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return _LOG_MAX_MB_DEFAULT
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-03 00:44:38 +08:00
|
|
|
|
def current_values() -> dict[str, Any]:
|
|
|
|
|
|
"""返回当前生效值(读取优先级:driver.config > .env)。"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
cfg = get_driver().config
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
cfg = None
|
|
|
|
|
|
|
|
|
|
|
|
if cfg is not None:
|
|
|
|
|
|
superusers = sorted(str(x) for x in getattr(cfg, "superusers", []) or [])
|
|
|
|
|
|
nickname = sorted(str(x) for x in getattr(cfg, "nickname", []) or [])
|
|
|
|
|
|
debug = bool(getattr(cfg, "debug", False))
|
|
|
|
|
|
log_level = _normalize_log_level(getattr(cfg, "log_level", "INFO"))
|
|
|
|
|
|
else:
|
|
|
|
|
|
superusers = _parse_list(_env_get("SUPERUSERS"))
|
|
|
|
|
|
nickname = _parse_list(_env_get("NICKNAME"))
|
|
|
|
|
|
debug = _env_get("DEBUG", "").strip().lower() in ("1", "true", "yes", "on")
|
|
|
|
|
|
log_level = _normalize_log_level(_env_get("LOG_LEVEL", "INFO"))
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"superusers": superusers,
|
|
|
|
|
|
"nickname": nickname,
|
|
|
|
|
|
"debug": bool(debug),
|
|
|
|
|
|
"log_level": log_level,
|
|
|
|
|
|
"log_level_options": LOG_LEVEL_OPTIONS,
|
2026-09-08 14:25:32 +08:00
|
|
|
|
"log_max_mb": _current_log_max_mb(),
|
|
|
|
|
|
"log_max_mb_default": _LOG_MAX_MB_DEFAULT,
|
2026-09-03 00:44:38 +08:00
|
|
|
|
"source": "config" if cfg is not None else "env",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _set_env_line(name: str, value: str) -> None:
|
|
|
|
|
|
"""替换 .env 中已有的 name= 行(保留其它行),不存在则追加。"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if _ENV_PATH.exists():
|
|
|
|
|
|
lines = _ENV_PATH.read_text(encoding="utf-8").splitlines()
|
|
|
|
|
|
else:
|
|
|
|
|
|
lines = []
|
|
|
|
|
|
replaced = False
|
|
|
|
|
|
out: list[str] = []
|
|
|
|
|
|
for line in lines:
|
|
|
|
|
|
stripped = line.strip()
|
|
|
|
|
|
if stripped.startswith(name + "=") or stripped.startswith(name + " ="):
|
|
|
|
|
|
if not replaced:
|
|
|
|
|
|
out.append(f"{name}={value}")
|
|
|
|
|
|
replaced = True
|
|
|
|
|
|
# 重复行跳过
|
|
|
|
|
|
else:
|
|
|
|
|
|
out.append(line)
|
|
|
|
|
|
if not replaced:
|
|
|
|
|
|
out.append(f"{name}={value}")
|
|
|
|
|
|
out_text = "\n".join(out).rstrip("\n") + "\n"
|
|
|
|
|
|
_ENV_PATH.write_text(out_text, encoding="utf-8")
|
|
|
|
|
|
except Exception as e: # noqa: BLE001
|
|
|
|
|
|
logger.warning(f"写入 .env({name}) 失败: {type(e).__name__}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _apply_log_level(log_level: str) -> None:
|
|
|
|
|
|
"""把 LOG_LEVEL 热应用到 loguru 的默认过滤配置。"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
from nonebot.log import logger as nb_logger
|
|
|
|
|
|
|
|
|
|
|
|
nb_logger.configure(extra={"nonebot_log_level": log_level})
|
|
|
|
|
|
except Exception as e: # noqa: BLE001
|
|
|
|
|
|
logger.warning(f"热应用 LOG_LEVEL 失败: {type(e).__name__}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_bot_settings(values: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
|
"""保存并尽量热应用。返回(新的当前值 + 提示)。"""
|
|
|
|
|
|
# 规范化输入
|
|
|
|
|
|
superusers = [str(x) for x in (values.get("superusers") or []) if str(x).strip()]
|
|
|
|
|
|
nickname = [str(x) for x in (values.get("nickname") or []) if str(x).strip()]
|
|
|
|
|
|
debug = bool(values.get("debug", False))
|
|
|
|
|
|
log_level = _normalize_log_level(values.get("log_level", "INFO"))
|
|
|
|
|
|
if log_level not in LOG_LEVEL_OPTIONS:
|
|
|
|
|
|
raise ValueError(f"LOG_LEVEL 必须是 {'/'.join(LOG_LEVEL_OPTIONS)} 之一")
|
|
|
|
|
|
|
2026-09-08 14:25:32 +08:00
|
|
|
|
# 日志清理大小(MB,写入 .env 的 HEXI_LOG_MAX_MB)
|
|
|
|
|
|
log_max_mb: int | None = None
|
|
|
|
|
|
_log_max_mb_raw = values.get("log_max_mb")
|
|
|
|
|
|
if _log_max_mb_raw is not None and str(_log_max_mb_raw).strip() != "":
|
|
|
|
|
|
try:
|
|
|
|
|
|
log_max_mb = int(float(_log_max_mb_raw))
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
raise ValueError("日志清理大小必须是正整数(MB)")
|
|
|
|
|
|
if log_max_mb <= 0:
|
|
|
|
|
|
raise ValueError("日志清理大小必须是正整数(MB)")
|
|
|
|
|
|
|
2026-09-03 00:44:38 +08:00
|
|
|
|
# 1. 写 os.environ(即时可读,进程内 getenv 立刻生效)
|
|
|
|
|
|
os.environ["SUPERUSERS"] = _serialize_list(superusers)
|
|
|
|
|
|
os.environ["NICKNAME"] = _serialize_list(nickname)
|
|
|
|
|
|
os.environ["DEBUG"] = "true" if debug else "false"
|
|
|
|
|
|
os.environ["LOG_LEVEL"] = log_level
|
2026-09-08 14:25:32 +08:00
|
|
|
|
if log_max_mb is not None:
|
|
|
|
|
|
os.environ["HEXI_LOG_MAX_MB"] = str(log_max_mb)
|
2026-09-03 00:44:38 +08:00
|
|
|
|
|
|
|
|
|
|
# 2. 落盘 .env(重启后依然生效)
|
|
|
|
|
|
_set_env_line("SUPERUSERS", _serialize_list(superusers))
|
|
|
|
|
|
_set_env_line("NICKNAME", _serialize_list(nickname))
|
|
|
|
|
|
_set_env_line("DEBUG", "true" if debug else "false")
|
|
|
|
|
|
_set_env_line("LOG_LEVEL", log_level)
|
2026-09-08 14:25:32 +08:00
|
|
|
|
if log_max_mb is not None:
|
|
|
|
|
|
_set_env_line("HEXI_LOG_MAX_MB", str(log_max_mb))
|
2026-09-03 00:44:38 +08:00
|
|
|
|
|
|
|
|
|
|
# 3. 热应用到 driver.config
|
|
|
|
|
|
applied: list[str] = []
|
|
|
|
|
|
try:
|
|
|
|
|
|
cfg = get_driver().config
|
|
|
|
|
|
if hasattr(cfg, "superusers"):
|
|
|
|
|
|
cfg.superusers = set(superusers)
|
|
|
|
|
|
applied.append("SUPERUSERS")
|
|
|
|
|
|
if hasattr(cfg, "nickname"):
|
|
|
|
|
|
cfg.nickname = set(nickname)
|
|
|
|
|
|
applied.append("NICKNAME")
|
|
|
|
|
|
# DEBUG 在 NoneBot Config 中属于 extra 字段,直接透传
|
|
|
|
|
|
try:
|
|
|
|
|
|
setattr(cfg, "debug", debug)
|
|
|
|
|
|
applied.append("DEBUG")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
if hasattr(cfg, "log_level"):
|
|
|
|
|
|
cfg.log_level = log_level
|
|
|
|
|
|
applied.append("LOG_LEVEL")
|
|
|
|
|
|
except Exception as e: # noqa: BLE001
|
|
|
|
|
|
logger.warning(f"热应用 driver.config 失败: {type(e).__name__}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
# 4. LOG_LEVEL 应用到 loguru
|
|
|
|
|
|
_apply_log_level(log_level)
|
2026-09-08 14:25:32 +08:00
|
|
|
|
if log_max_mb is not None:
|
|
|
|
|
|
applied.append("HEXI_LOG_MAX_MB")
|
2026-09-03 00:44:38 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"ok": True,
|
|
|
|
|
|
**current_values(),
|
|
|
|
|
|
"applied": applied,
|
|
|
|
|
|
"restart_required": True,
|
|
|
|
|
|
"notice": "已保存到 .env 并尽量热应用;部分插件在 import 时缓存了 SUPERUSERS/NICKNAME,"
|
|
|
|
|
|
"如需完全生效,请在下方执行「重启 bot」。",
|
|
|
|
|
|
}
|