Files
MusicBot/musicbot/main.py
T
snowy-yang a11c0b375f 新增热重载:文件变更自动重启(开发用,默认关)
- musicbot/reload.py:轮询监控 musicbot/ 下的 .py 与 intents.toml/.env,
  变更后重启 bot 子进程;跳过 logs/、__pycache__/、.venv/
- 进程级重启而非模块重载:长连接 + 模块级状态 + handler 闭包,
  就地重载易留幽灵 handler,重启子进程状态干净
- 子进程用 _MUSICBOT_RELOAD_CHILD 标记,不会套娃自我复制
- Windows 用 taskkill /T 收整棵进程树,实测连续重载无进程泄漏
- 开启方式:`uv run musicbot --reload`(start.bat 已带)或 RELOAD=on
- 监督者不配日志,避免与子进程抢写同一日志文件
2026-09-20 22:44:20 +08:00

91 lines
3.4 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.
"""装配入口:配置日志、注册命令并运行 bot(可选热重载)。"""
import argparse
import asyncio
import sys
from pathlib import Path
from aiohttp import ClientSession
from loguru import logger
from qqbotsdk import EventEmitter, run_loop
from . import reload
from .config import BotConfig
from .music import register
# 日志文件按天轮转、保留 7 天;目录写不进去时退化为仅控制台
_LOG_RETENTION = "7 days"
_LOG_FORMAT = (
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
"<level>{level: <8}</level> | "
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
"<level>{message}</level>"
)
def setup_logging(config: BotConfig) -> None:
"""日志同时输出到控制台与 logs/musicbot.log(按天轮转)。"""
logger.remove() # 去掉 loguru 默认的 stderr handler,统一由这里接管
logger.add(sys.stderr, level=config.log_level, format=_LOG_FORMAT)
try:
config.log_dir.mkdir(parents=True, exist_ok=True)
logger.add(
config.log_dir / "musicbot.log",
level=config.log_level,
format=_LOG_FORMAT,
rotation="00:00", # 每天零点切分
retention=_LOG_RETENTION,
encoding="utf-8",
enqueue=True, # 多任务写入串行化,避免并发写坏文件
backtrace=True,
diagnose=False, # 不回显变量值,避免把凭证写进日志
)
except OSError as e:
logger.warning(f"日志目录不可写({config.log_dir}),仅输出到控制台: {e}")
async def amain() -> None:
config = BotConfig.load()
setup_logging(config)
logger.info(f"musicbot 启动,日志级别 {config.log_level}")
ee = EventEmitter() # 队列由 emitter 自建,run_loop 复用同一实例
# bot 自建连接池:直链探测不受 SDK 的 TIMEOUT 约束
async with ClientSession() as http:
register(ee, config, http)
await run_loop(ee)
def _parse_args(argv: list[str] | None) -> argparse.Namespace:
parser = argparse.ArgumentParser(prog="musicbot", description="QQ 群点歌 bot")
parser.add_argument(
"--reload",
action="store_true",
help="开发用:监控源码与配置,变更后自动重启(等价于 RELOAD=on)",
)
return parser.parse_args(argv)
def _project_root() -> Path:
"""项目根:包目录的上一级(flat 布局)。找不到 pyproject.toml 时退回当前
工作目录,避免安装成非 editable 包时监视到 site-packages。"""
root = Path(__file__).resolve().parent.parent
return root if (root / "pyproject.toml").is_file() else Path.cwd()
def main(argv: list[str] | None = None) -> None:
args = _parse_args(argv)
config = BotConfig.load()
# 子进程只跑 bot,绝不再套一层监督(否则 --reload 会无限自我复制)
reload_on = (args.reload or config.reload) and not reload.is_child()
if reload_on:
# 监控与子进程 cwd 都用项目根:intents.toml/.env 都在那里,
# 保证「监控的树」与「实际加载的配置」一致
root = _project_root()
# 监督者不配置日志:子进程自己会配,避免两边抢写同一个日志文件
code = reload.run_supervisor([sys.executable, "-m", "musicbot"], root)
sys.exit(code)
try:
asyncio.run(amain())
except KeyboardInterrupt:
logger.info("musicbot 已退出")