feat(mc-server-status): 服务器列表 Web 管理页 / 数据层抽离 / mcstatus 13 适配

- services/servers.py:校验 + 读写 + 增删改收敛成一份数据层(顶层不 import nonebot,
  可脱离运行时单测),群命令与 Web API 都只经它改数据,校验不再各写一套
- web_hub.py + /hub/mc_server_status 页面:按群列出服务器,抽屉新增/编辑(改名)/
  测试连接,行内删除(删空一台连带删群);每条路由自带 require_admin
- handlers/server.py 改为薄封装,只在需要命令特有提示时自己判断
- services/mc.py 的图标属性名兼容 mcstatus ≥13 的 icon(旧版叫 favicon,
  读错会把整条查询打成"查询失败")
- tests/test_mc_servers.py:数据层归一/改名/删群/坏文件兜底 + 图标解码两版属性名

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-22 14:23:46 +08:00
co-authored by Claude Code
parent 4badcfcf32
commit 814c1994b6
9 changed files with 1150 additions and 46 deletions
@@ -5,15 +5,31 @@ from __future__ import annotations
from base64 import b64decode
from io import BytesIO
from re import findall
from typing import Union
from mcstatus import BedrockServer, JavaServer
from nonebot.adapters.onebot.v11 import Message, MessageSegment as MS
def _server_icon(status) -> bytes | None:
"""取服务器图标字节;没有图标或格式不认识就返回 None。
属性名跨版本换过:mcstatus ≥13 叫 `icon`,11.x/12.x 叫 `favicon`
(13.0.0 的 changelog:`JavaStatusResponse.favicon` → `icon`,值没变)。
值可能是完整的 data URI,也可能是不带前缀的裸 base64,两种都认。
"""
raw = getattr(status, "icon", None) or getattr(status, "favicon", None)
if not raw:
return None
data = raw.split("base64,", 1)[-1] if "base64," in raw else raw
try:
return b64decode(data)
except Exception: # noqa: BLE001 — 图标坏了不影响其它字段
return None
async def check_mc_status(
name: str, host: str, server_type: str
) -> Union[str, Message]:
) -> str | Message:
try:
if server_type == "js":
js = await JavaServer.async_lookup(host, timeout=2)
@@ -38,11 +54,8 @@ async def check_mc_status(
else:
player_list = "没人在线"
latency = round(status.latency)
if status.favicon:
aa, bb = status.favicon.split("base64,")
icon = MS.image(BytesIO(b64decode(bb))) + "\n"
else:
icon = ""
icon_data = _server_icon(status)
icon = MS.image(BytesIO(icon_data)) + "\n" if icon_data else ""
msg = (
icon
+ f"名称:{name}\n版本:{version}\n在线:{online}\n延迟:{latency}ms\n在线列表:\n{player_list}"