Files
MusicBot/musicbot/config.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

90 lines
3.7 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 os
from dataclasses import dataclass
from pathlib import Path
from urllib.parse import urlsplit
# 内嵌键盘上限 5 行 × 每行 5 个;歌曲占行 + 1 行翻页 → 每页最多 20 首
_MAX_MENU_PAGE_SIZE = 20
# PUBLIC_URL 不带端口时文件代理的监听端口(MusicServer 默认在 16688)
_DEFAULT_PROXY_PORT = 16689
_LOG_LEVELS = ("TRACE", "DEBUG", "INFO", "SUCCESS", "WARNING", "ERROR", "CRITICAL")
def _flag(name: str, default: bool) -> bool:
"""读布尔环境变量:on/true/1/yes 为真,off/false/0/no 为假。"""
raw = os.getenv(name, "").strip().lower()
if not raw:
return default
return raw in ("1", "true", "on", "yes")
def _send_mode() -> str:
"""SEND_MODE:下载发送方式。
file 先上传群文件、失败回退直链;link 直接发直链(跳过上传,可避开
"仅群主和管理员上传文件" 的 40093005)。非法值按默认 link 处理。
"""
raw = os.getenv("SEND_MODE", "").strip().lower()
return raw if raw in ("file", "link") else "link"
def _proxy_port(public_url: str) -> int:
"""PROXY_PORT:文件代理的监听端口。
缺省跟随 PUBLIC_URL 里的显式端口(如 http://1.2.3.4:16689 → 16689),
没写端口用默认(https 域名走反向代理时必须显式配置,bot 本身不收 TLS)。"""
raw = os.getenv("PROXY_PORT", "").strip()
if raw.isdigit():
return int(raw)
try:
return urlsplit(public_url).port or _DEFAULT_PROXY_PORT
except ValueError: # URL 端口段写坏了
return _DEFAULT_PROXY_PORT
@dataclass(frozen=True, slots=True)
class BotConfig:
music_server_url: str
music_quality: str
music_source: str
menu_page_size: int = 10 # 选歌菜单每页候选数(1~20)
withdraw: bool = True # 自动撤回旧菜单/卡片是否开启
send_mode: str = "link" # 下载发送方式:"file" 先传文件 / "link" 直接发直链
file_proxy: bool = False # 文件代理开关(FILE_PROXY=on 且配了 PUBLIC_URL 才开)
public_url: str = "" # 文件代理对外基址(PUBLIC_URL,如 http://1.2.3.4:16689)
proxy_port: int = _DEFAULT_PROXY_PORT # 文件代理监听端口(缺省跟 PUBLIC_URL)
command_panel: bool = True # 启动/重连时自动注册指令面板(输入框 / 菜单)
log_level: str = "INFO" # 控制台与文件日志级别
log_dir: Path = Path("logs") # 日志目录;写不进去时自动退化为仅控制台
reload: bool = False # 热重载(开发用;--reload 亦可开启)
@classmethod
def load(cls) -> "BotConfig":
page_size = int(os.getenv("MENU_PAGE_SIZE", "10"))
public_url = os.getenv("PUBLIC_URL", "").strip().rstrip("/")
return cls(
music_server_url=os.getenv("MUSIC_SERVER_URL", "http://127.0.0.1:16688"),
music_quality=os.getenv("MUSIC_QUALITY", "128k"),
music_source=os.getenv("MUSIC_SOURCE", "tx"),
menu_page_size=min(max(1, page_size), _MAX_MENU_PAGE_SIZE),
withdraw=_flag("WITHDRAW", True),
send_mode=_send_mode(),
file_proxy=_flag("FILE_PROXY", True) and bool(public_url),
public_url=public_url,
proxy_port=_proxy_port(public_url),
command_panel=_flag("COMMAND_PANEL", True),
log_level=_log_level(),
log_dir=Path(os.getenv("LOG_DIR", "logs")),
reload=_flag("RELOAD", False),
)
def _log_level() -> str:
"""LOG_LEVEL:日志级别,非法值按 INFO。"""
raw = os.getenv("LOG_LEVEL", "").strip().upper()
return raw if raw in _LOG_LEVELS else "INFO"