Files
MusicBot/musicbot/main.py
T
snowy-yang c364506925
代码测试 / ruff + pytest (push) Canceled after 0s
feat: 文件代理、群消息防双发、下载指令出选歌菜单与音频平台选集
- 文件代理(PUBLIC_URL):上传与直链文本的 URL 换成 bot 公开地址,请求
  到来现场重解析直链再转发字节,绕开 QQ 抓风控直链失败(40093007)与
  qs 本地流外网不可达;PROXY_PORT 指定监听端口
- 群消息去重补第二把钥匙:全量模式下 @消息同时命中两事件且 msg_id 不同,
  只按 msg_id 拦不住,再加「群+发送人+文本」10 秒短窗(修双发)
- 「<平台>下载 <歌名>」改出该平台的选歌菜单(与 /点歌 同款交互、spage
  翻页锁定平台),不再直接下第一首
- 猫耳/懒人/蜻蜓节目级条目点编号先出选集菜单(/album):猫耳选集接口
  忽略翻页参数,一次拉 50 集本地切片;懒人/蜻蜓走服务端翻页;空选集
  (已下线/需登录)给明确提示
- 指令面板条目按 /sources 现算、按宽度裁剪(修 30013 超限);装配入口
  改用 Bot 门面;qqbotsdk 依赖更名 qqbotsdk-py(撤 GitHub archive 源)
2026-10-03 23:55:33 +08:00

95 lines
3.6 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 Bot
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}")
bot = Bot() # 构造即读 .env 并校验 SDK 配置(APPID/APPSECRET/CONNECTER)
# bot 自建连接池:直链探测不受 SDK 的 TIMEOUT 约束
async with ClientSession() as http:
register(bot, config, http)
await bot.start()
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()
# 监督者不配置日志:子进程自己会配,避免两边抢写同一个日志文件
try:
code = reload.run_supervisor([sys.executable, "-m", "musicbot"], root)
except KeyboardInterrupt:
# 兜底 run_supervisor 循环间隙的 Ctrl+C,同样不打印 traceback
code = 0
sys.exit(code)
try:
asyncio.run(amain())
except KeyboardInterrupt:
logger.info("musicbot 已退出")