代码测试 / ruff + pytest (push) Canceled after 0s
- 文件代理(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 源)
95 lines
3.6 KiB
Python
95 lines
3.6 KiB
Python
"""装配入口:配置日志、注册命令并运行 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 已退出")
|