♻️ refactor(command): 配置系统从 SQLite 迁移至 YAML 并修复白名单失效

- 用 CommandConfig dataclass 单例替代模块级变量,解决 from import 造成的本地绑定不随 global 更新的 bug
- 删除 db.py,改用 settings.yaml 存储动态配置,首次启动自动创建并合并 .env 默认值
- 新增文件轮询 watcher(2 秒),检测 YAML 变更自动热重载
- 管理界面 API 改为直接读写 YAML,即时生效
- 依赖 aiosqlite 替换为 pyyaml
This commit is contained in:
2026-05-03 18:23:29 +08:00
parent 9ffe78a9c2
commit f82363f45f
8 changed files with 314 additions and 270 deletions

View File

@@ -8,24 +8,15 @@ from aiohttp import web
from ncatbot.plugin import NcatBotPlugin
from .config import (
COMMAND_ALLOWED_GROUPS,
COMMAND_ALLOWED_USERS,
COMMAND_AT_SENDER,
COMMAND_CALLBACK_URL,
COMMAND_DENIED_GROUPS,
COMMAND_DENIED_USERS,
COMMAND_LENGTH_MAX,
COMMAND_LENGTH_MIN,
COMMAND_LIST_MODE,
COMMAND_PREFIX,
COMMAND_SCOPE,
HOST,
PORT,
UPLOAD_DIR,
WEBHOOK_API_KEY,
SETTINGS_YAML_PATH,
command,
ensure_settings_yaml,
reload_settings,
)
from .db import get_settings, init_db
from .handlers.admin import admin_page_handler, api_get_settings, api_reload_settings, api_update_settings
from .handlers.command import parse_command, send_command_callback
from .handlers.health import health_handler
@@ -47,12 +38,12 @@ class WebHookPlugin(NcatBotPlugin):
self._webhook_runner: web.AppRunner | None = None
self._cleanup_task: asyncio.Task | None = None
self._listener_task: asyncio.Task | None = None
self._watcher_task: asyncio.Task | None = None
async def on_load(self):
# 初始化数据库并加载动态配置
await init_db()
settings = await get_settings()
reload_settings(settings)
# 初始化 settings.yaml 并加载配置
ensure_settings_yaml()
reload_settings()
self.logger.info("Webhook 插件已加载")
self.logger.info(
@@ -61,32 +52,28 @@ class WebHookPlugin(NcatBotPlugin):
)
self.logger.info(
"命令监听: 前缀=%s 长度=%d~%d 范围=%s 名单=%s 回调=%s",
COMMAND_PREFIX,
COMMAND_LENGTH_MIN,
COMMAND_LENGTH_MAX,
COMMAND_SCOPE,
COMMAND_LIST_MODE,
COMMAND_CALLBACK_URL or "未配置",
command.prefix,
command.length_min,
command.length_max,
command.scope,
command.list_mode,
command.callback_url or "未配置",
)
asyncio.create_task(self._start_webhook())
self._cleanup_task = asyncio.create_task(self._cleanup_loop())
self._listener_task = asyncio.create_task(self._message_listener())
self._watcher_task = asyncio.create_task(self._config_watcher())
async def on_close(self):
if self._listener_task is not None:
self._listener_task.cancel()
try:
await self._listener_task
except asyncio.CancelledError:
pass
self._listener_task = None
if self._cleanup_task is not None:
self._cleanup_task.cancel()
try:
await self._cleanup_task
except asyncio.CancelledError:
pass
self._cleanup_task = None
for task_attr in ("_watcher_task", "_listener_task", "_cleanup_task"):
task = getattr(self, task_attr)
if task is not None:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
setattr(self, task_attr, None)
await self._stop_webhook()
self.logger.info("Webhook 插件已卸载")
@@ -99,6 +86,21 @@ class WebHookPlugin(NcatBotPlugin):
except Exception as exc:
self.logger.error("清理过期文件失败: %s", exc)
async def _config_watcher(self) -> None:
"""轮询 settings.yaml 的 mtime变更时热重载配置。"""
last_mtime: float = 0.0
while True:
await asyncio.sleep(2)
try:
if SETTINGS_YAML_PATH.exists():
current_mtime = SETTINGS_YAML_PATH.stat().st_mtime
if current_mtime != last_mtime:
last_mtime = current_mtime
reload_settings()
self.logger.info("settings.yaml changed, config reloaded")
except Exception as exc:
self.logger.error("Config watcher error: %s", exc)
async def _message_listener(self) -> None:
"""监听 QQ 消息,匹配命令模式后转发到外部回调。"""
try:
@@ -114,25 +116,25 @@ class WebHookPlugin(NcatBotPlugin):
# 范围过滤group / private / all
is_group = hasattr(event.data, "group_id")
if COMMAND_SCOPE == "group" and not is_group:
if command.scope == "group" and not is_group:
continue
if COMMAND_SCOPE == "private" and is_group:
if command.scope == "private" and is_group:
continue
# 黑白名单过滤
if COMMAND_LIST_MODE == "allow":
if command.list_mode == "allow":
# 白名单模式:在名单内才放行
if COMMAND_ALLOWED_GROUPS and is_group:
if event.data.group_id not in COMMAND_ALLOWED_GROUPS:
if command.allowed_groups and is_group:
if event.data.group_id not in command.allowed_groups:
continue
if COMMAND_ALLOWED_USERS and event.data.user_id not in COMMAND_ALLOWED_USERS:
if command.allowed_users and event.data.user_id not in command.allowed_users:
continue
elif COMMAND_LIST_MODE == "deny":
elif command.list_mode == "deny":
# 黑名单模式:在名单内则拒绝
if COMMAND_DENIED_GROUPS and is_group:
if event.data.group_id in COMMAND_DENIED_GROUPS:
if command.denied_groups and is_group:
if event.data.group_id in command.denied_groups:
continue
if COMMAND_DENIED_USERS and event.data.user_id in COMMAND_DENIED_USERS:
if command.denied_users and event.data.user_id in command.denied_users:
continue
# 构建回调数据
@@ -188,4 +190,4 @@ class WebHookPlugin(NcatBotPlugin):
if self._webhook_runner is not None:
await self._webhook_runner.cleanup()
self._webhook_runner = None
self.logger.info("Webhook 已停止")
self.logger.info("Webhook 已停止")