"""装配入口:配置日志、注册命令并运行 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 = (
"{time:YYYY-MM-DD HH:mm:ss.SSS} | "
"{level: <8} | "
"{name}:{function}:{line} - "
"{message}"
)
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 已退出")