"""指令面板(群里输入框敲 / 唤出的指令菜单)自动注册。 调开放平台 2026-08 新增的 /v2/panels 接口,把点歌指令同步到 QQ: 启动与每次重连(READY)各同步一次——没有就创建(target_type=all 对全部群生效),内容有变就更新,一致则跳过,幂等可重入。 条目由调用方给定(music.panel_items 按 MusicServer /sources 现算: 基础指令 + 热搜/榜单 + 各平台「<平台>下载」),服务器增删源跟着变。 接口上限(每面板 20 条;name/desc 按宽度限 14/30——汉字计 2)由条目 生成方裁剪。失败只记日志不影响运行(如功能未对机器人开放)。 """ from typing import Any from aiohttp import ClientError from loguru import logger from qqbotsdk.api import ApiError, BotApi _API_FAILS = (ApiError, TimeoutError, ClientError) # 面板挂在群聊场景,对所有群生效 _SCOPE = "group" _TARGET_TYPE = "all" # remark 兼作身份标识:重连同步时按它找到自己注册过的面板 _REMARK = "音乐bot" def _signature(items: list[dict]) -> list[tuple]: """面板条目的比较指纹:只取本模块管理的字段,忽略其余与键序。""" return [ ( i.get("name"), i.get("type"), i.get("desc"), i.get("only_admin", False), i.get("link"), ) for i in items ] def _unwrap(body: Any, key: str) -> Any: """响应可能整体包一层 {"data": ...},也可能直接是数据本体,两者都认。""" if not isinstance(body, dict): return body inner = body.get("data") return inner if key not in body and isinstance(inner, dict) else body def _mine(records: list[Any]) -> dict | None: """在面板列表里找本 bot 注册过的那张(按 remark 识别)。""" for record in records: if not isinstance(record, dict): continue if (record.get("panel") or {}).get("remark") == _REMARK: return record return None async def sync_command_panel(api: BotApi, items: list[dict]) -> None: """把给定条目同步成 QQ 指令面板(幂等)。任何失败只记日志,绝不抛出。""" try: resp = await api.get("/v2/panels", params={"scope": _SCOPE, "limit": "20"}) records = _unwrap(resp, "records").get("records") or [] mine = _mine(records) unchanged = mine and _signature(mine["panel"].get("items") or []) == _signature( items ) if unchanged: logger.info(f"指令面板已是最新({len(items)} 条指令)") return if mine: await api.put( f"/v2/panels/{mine['panel_id']}", json={"panel": {"items": items, "remark": _REMARK}}, ) logger.info(f"指令面板已更新({len(items)} 条指令)") else: body = { "scope": _SCOPE, "panel": {"items": items, "remark": _REMARK}, "target_type": _TARGET_TYPE, } await api.post("/v2/panels", json=body) logger.info(f"指令面板已创建({len(items)} 条指令,对全部群生效)") except _API_FAILS as e: # 失败不影响运行;指令可改为在开放平台后台手工配置 logger.warning(f"指令面板同步失败: {e!r}")