Files
HeXi/hexi/plugins/nonebot_plugin_web_hub/bot_settings.py
T
sansenhoshiandClaude 9371a28e35 refactor: restructure plugins per MTSS standard, consolidate assets to res/
Per docs/plugin-audit-report.md (plugins normalized to
Trigger(handlers) → Service(services) → Model(repository/models) + utils):

- Split monolithic __init__.py into handlers/services/utils across
  dailywife, deer_pipe, dice, galgame_card, helldivers_tools,
  huoziyinshua, learning_chat, makeaquote, mc_server_status,
  ncm_saying, picfinder_take, picstatus, random_jm_code, regif,
  steam_info, video_analysis, group_tools
- Move static assets under res/: deer_pipe font/img, makeaquote font,
  helldivers img/templates, huoziyinshua HuoZiYinShua
- Add config.py + register_config_items to ncm_saying, random_jm_code,
  group_tools; learning_chat unified config bridge
- Remove deprecated: voice_trans plugin, bf_bot/test.py, dead code in
  dailywife/deer_pipe, empty dirs, debug scripts under helldivers temp
- Disable brash_general_supercredits_tools (stub comment only)
- bot.py: optional stdout/stderr redirect to log file for Web log viewer,
  force ANSI colorize on non-TTY sinks
- Move runtime data (jm_code.json) out of plugin dir into hexi/data
- Docs: plugin-audit-report.md; README reflects removed plugins

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-03 00:44:38 +08:00

201 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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
_REPO_ROOT = Path(__file__).resolve().parents[3]
# 仓库根目录的 .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"]
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()
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,
"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)} 之一")
# 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
# 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)
# 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)
return {
"ok": True,
**current_values(),
"applied": applied,
"restart_required": True,
"notice": "已保存到 .env 并尽量热应用;部分插件在 import 时缓存了 SUPERUSERS/NICKNAME,"
"如需完全生效,请在下方执行「重启 bot」。",
}