升级到新版 SDK 并补充点歌业务日志

- 锁定 qqbotsdk b37a53f(含:健康断开零等待重连、静默常规连接日志、
  失败才记断开与退避)
- 新增业务日志:启动打印生效配置、点歌/查歌词/帮助、菜单已发、
  选歌、按钮点击、语音/文件/直链已发,失败项统一带群号前缀
  (此前 18 处日志 17 处是 warning,成功路径完全静默,故日志里
  看不到任何点歌消息)
This commit is contained in:
snowy-yang committed 2026-09-21 18:26:23 +08:00
1 parent a11c0b375f
commit 6b6492dc8d
3 files changed
+20 -19

No files matched your search

+18 -4
View File
@@ -404,6 +404,7 @@ async def send_menu(
logger.warning(f"文本菜单发送失败: {e!r}") logger.warning(f"文本菜单发送失败: {e!r}")
return return
await _swap_message(api, config, group_openid, resp) await _swap_message(api, config, group_openid, resp)
logger.info(f"[{group_openid}] 菜单已发:{keyword} 第{page}页 {len(songs)}首")
async def send_card( async def send_card(
@@ -451,6 +452,7 @@ async def send_card(
logger.warning(f"文本卡片发送失败: {e!r}") logger.warning(f"文本卡片发送失败: {e!r}")
return return
await _swap_message(api, config, group_openid, resp) await _swap_message(api, config, group_openid, resp)
logger.info(f"[{group_openid}] 选歌:{title}")
async def _upload_group_file( async def _upload_group_file(
@@ -489,6 +491,7 @@ async def send_song(
无论成败都撤回卡片。""" 无论成败都撤回卡片。"""
found = await _fetchable_play(http, config, song, keyword) found = await _fetchable_play(http, config, song, keyword)
if found is None: if found is None:
logger.warning(f"[{group_openid}] 无可用直链:{song.name}")
await _reply( await _reply(
api, group_openid, f"没拿到「{song.name}」的播放链接", msg_id, event_id api, group_openid, f"没拿到「{song.name}」的播放链接", msg_id, event_id
) )
@@ -510,26 +513,28 @@ async def send_song(
) )
if sent := _sent_id(resp): if sent := _sent_id(resp):
_mark(sent) _mark(sent)
logger.info(f"[{group_openid}] {label}已发:{_caption(song)}")
return return
except _API_FAILS as e: except _API_FAILS as e:
no_permission = isinstance(e, ApiError) and e.code == _NO_FILE_PERMISSION no_permission = isinstance(e, ApiError) and e.code == _NO_FILE_PERMISSION
if no_permission: if no_permission:
logger.warning("群文件上传仅限群主/管理员,改为直链文本") logger.warning(f"[{group_openid}] 群文件上传无权限,改为直链文本")
else: else:
logger.warning(f"以{label}发送失败: {e!r}") logger.warning(f"[{group_openid}] 以{label}发送失败: {e!r}")
# 回退直链文本;文件权限问题多给一句说明 # 回退直链文本;文件权限问题多给一句说明
text = f"{_caption(song)}\n{url}" text = f"{_caption(song)}\n{url}"
if no_permission: if no_permission:
text = f"{_caption(song)}\n(本群文件上传仅限群主/管理员,先给直链)\n{url}" text = f"{_caption(song)}\n(本群文件上传仅限群主/管理员,先给直链)\n{url}"
try: try:
await _reply(api, group_openid, text, msg_id, event_id) await _reply(api, group_openid, text, msg_id, event_id)
logger.info(f"[{group_openid}] 直链已发:{_caption(song)}")
except _API_FAILS as e: except _API_FAILS as e:
# 被动回复被拒(如互动 event_id 超时效)→ 降级为主动消息 # 被动回复被拒(如互动 event_id 超时效)→ 降级为主动消息
logger.warning(f"被动回复失败,降级为主动消息: {e!r}") logger.warning(f"[{group_openid}] 被动回复失败,降级为主动消息: {e!r}")
try: try:
await _reply(api, group_openid, text) await _reply(api, group_openid, text)
except _API_FAILS as e: except _API_FAILS as e:
logger.warning(f"文本链接发送失败: {e!r}") logger.warning(f"[{group_openid}] 文本链接发送失败: {e!r}")
async def send_lyric( async def send_lyric(
@@ -688,14 +693,17 @@ async def handle_command(
选歌/播放/下载现在都由内嵌按钮(回调)承担,这里只处理文字命令。""" 选歌/播放/下载现在都由内嵌按钮(回调)承担,这里只处理文字命令。"""
if match := _SONG_RE.search(content): if match := _SONG_RE.search(content):
keyword = match.group(1).strip() keyword = match.group(1).strip()
logger.info(f"[{group_openid}] 点歌:{keyword}")
await send_menu(api, http, config, group_openid, keyword, msg_id=msg_id) await send_menu(api, http, config, group_openid, keyword, msg_id=msg_id)
return True return True
if match := _LYRIC_RE.search(content): if match := _LYRIC_RE.search(content):
keyword = match.group(1).strip() keyword = match.group(1).strip()
logger.info(f"[{group_openid}] 查歌词:{keyword}")
if song := await _find(api, http, config, group_openid, msg_id, keyword): if song := await _find(api, http, config, group_openid, msg_id, keyword):
await send_lyric(api, http, config, group_openid, song, msg_id=msg_id) await send_lyric(api, http, config, group_openid, song, msg_id=msg_id)
return True return True
if "音乐帮助" in content: if "音乐帮助" in content:
logger.info(f"[{group_openid}] 查看帮助")
await _reply(api, group_openid, _HELP, msg_id=msg_id) await _reply(api, group_openid, _HELP, msg_id=msg_id)
return True return True
return False return False
@@ -732,6 +740,11 @@ def register(ee: EventEmitter, config: BotConfig, http: ClientSession) -> None:
"""把点歌命令与按钮回调 handler 挂到 emitter(群场景);http 为 bot """把点歌命令与按钮回调 handler 挂到 emitter(群场景);http 为 bot
自建连接池(不受 SDK 的 TIMEOUT 约束),经闭包供 handler 使用——不能 自建连接池(不受 SDK 的 TIMEOUT 约束),经闭包供 handler 使用——不能
用形参注入,否则拿到的是 SDK 那个 5s 超时的连接池。""" 用形参注入,否则拿到的是 SDK 那个 5s 超时的连接池。"""
logger.info(
f"点歌已就绪:源={config.music_source or '聚合'} 音质={config.music_quality} "
f"每页={config.menu_page_size} 下载={config.send_mode} "
f"自动撤回={'开' if config.withdraw else '关'}"
)
# @机器人 的消息走前一个事件;开通「接收所有消息」后群内全部消息走 # @机器人 的消息走前一个事件;开通「接收所有消息」后群内全部消息走
# 全量事件,两个事件字段一致,共用同一个 handler # 全量事件,两个事件字段一致,共用同一个 handler
@@ -743,6 +756,7 @@ def register(ee: EventEmitter, config: BotConfig, http: ClientSession) -> None:
@ee.on("INTERACTION_CREATE") @ee.on("INTERACTION_CREATE")
async def on_button(act: Interaction, ev: Event, api: BotApi) -> None: async def on_button(act: Interaction, ev: Event, api: BotApi) -> None:
if act.chat_type == 1 and act.id and act.group_openid: if act.chat_type == 1 and act.id and act.group_openid:
logger.info(f"[{act.group_openid}] 按钮:{act.button_data[:40]}")
# 被动回复的 event_id 用网关帧最外层 id(Event.event_id), # 被动回复的 event_id 用网关帧最外层 id(Event.event_id),
# 裸 uuid(act.id)只用于回应互动 # 裸 uuid(act.id)只用于回应互动
await handle_button( await handle_button(
+1 -1
View File
@@ -1,7 +1,7 @@
"""开发用热重载:监控源码与配置文件,变更后重启 bot 子进程。 """开发用热重载:监控源码与配置文件,变更后重启 bot 子进程。
进程级重启(而非进程内模块重载):bot 是长连接 + 模块级状态 + handler 进程级重启(而非进程内模块重载):bot 是长连接 + 模块级状态 + handler
闭包,就地重载要清理 pyee 监听器与旧闭包,容易留下幽灵 handler;重启 闭包,就地重载要清理事件监听器与旧闭包,容易留下幽灵 handler;重启
子进程则状态与闭包都是干净的,代价是重连一次 websocket。 子进程则状态与闭包都是干净的,代价是重连一次 websocket。
监督进程只负责拉起/杀掉子进程与轮询文件改动,自身不跑 bot;子进程由 监督进程只负责拉起/杀掉子进程与轮询文件改动,自身不跑 bot;子进程由
Generated
+1 -14
View File
@@ -628,18 +628,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
] ]
[[package]]
name = "pyee"
version = "14.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1d/f1/fdedc2c75c3e31a330659c85e5793bb18b3397981fbf0844c6dee5b18926/pyee-14.0.0.tar.gz", hash = "sha256:76dd0f4314ecd27f02dc73589dea7fd3853f9b6176d8ef9b122860657e3602de", size = 98760, upload-time = "2026-08-13T04:26:11.021Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/12/5347938b1f9a6453f0dbdfcc3e2388a1320ef9b9ec17fbefbc4ab647ea98/pyee-14.0.0-py3-none-any.whl", hash = "sha256:3ac2d3229a9677f7de2c33d7f52fe25b638a46b19c413fea2edc8c6d0a644e4d", size = 15553, upload-time = "2026-08-13T04:26:09.916Z" },
]
[[package]] [[package]]
name = "pygments" name = "pygments"
version = "2.21.0" version = "2.21.0"
@@ -702,12 +690,11 @@ wheels = [
[[package]] [[package]]
name = "qqbotsdk" name = "qqbotsdk"
version = "0.1.0" version = "0.1.0"
source = { git = "https://gitea.4i.hk/hhhge/qqbotsdk.git#71cbfdaf5d4814d8798304c6e53b5acb6266f1c2" } source = { git = "https://gitea.4i.hk/hhhge/qqbotsdk.git#b37a53f6e0dec360a3e684fbafd7140ec00a2711" }
dependencies = [ dependencies = [
{ name = "aiohttp" }, { name = "aiohttp" },
{ name = "cryptography" }, { name = "cryptography" },
{ name = "loguru" }, { name = "loguru" },
{ name = "pyee" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "ujson" }, { name = "ujson" },
] ]