Files
HeXi/hexi/plugins/nonebot_plugin_mc_server_status/web_hub.py
T
sansenhoshiandClaude Code 814c1994b6 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>
2026-09-22 14:23:46 +08:00

160 lines
6.2 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.
"""MC 服务器信息查询 —— Web API 子应用(挂载到 /api/mc_server_status)。
服务器数据(`hexi/data/mc_status_data.json`:`{群号: {名字: [地址, 类型]}}`)
的唯一 Web 读写入口:列出 / 新增 / 编辑(含改名)/ 删除 / 测试连接。
写操作全部落在 services/servers.py(校验只有那一份),和群里的
`添加服务器`/`删除服务器` 命令改的是同一份内存态 + 同一个文件。
鉴权走 hexi.web_hub.web_auth(OAuth2 + SQLite),与统一管理台 /hub 共用登录态。
**每条路由都要自带 `dependencies=[auth]`** —— mount 层没有兜底,漏一条就是匿名可访问。
前端页面:hexi/web/src/plugins/mc_server_status/index.tsx。
"""
from __future__ import annotations
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from hexi.web_hub.web_auth import require_admin
from .services import mc as mc_service
from .services import servers as store
API = require_admin
def _ok(data=None, msg: str = "ok") -> JSONResponse:
return JSONResponse({"status": 0, "msg": msg, "data": data})
def _fail(msg: str, status: int = 400) -> JSONResponse:
return JSONResponse({"status": status, "msg": msg})
async def _group_names() -> dict[str, str]:
"""群号 → 群名;拿不到 bot(未连接)时返回空表,前端只显示群号。"""
try:
from nonebot import get_adapter
from nonebot.adapters.onebot.v11 import Adapter
bots = get_adapter(Adapter).bots
bot = next(iter(bots.values()), None)
if bot is None:
return {}
return {
str(g["group_id"]): g.get("group_name") or ""
for g in await bot.get_group_list()
}
except Exception: # noqa: BLE001 — 未连接/适配器未加载都按拿不到处理
return {}
def _payload() -> dict:
"""整表视图:群列表(含群名与在线标记)+ 类型选项。"""
groups = store.snapshot()
return {
"groups": [
{
"group_id": str(gid),
"servers": [
{"name": name, "address": value[0], "type": value[1]}
for name, value in sorted(servers.items())
],
}
for gid, servers in sorted(groups.items())
],
"types": store.SERVER_TYPES,
"address_hint": store.ADDRESS_HINT,
}
async def _payload_with_names() -> dict:
"""跑一次 payload 再把群名补上(两条路由共用的收尾)。"""
data = _payload()
names = await _group_names()
for row in data["groups"]:
row["group_name"] = names.get(row["group_id"], "")
row["online"] = row["group_id"] in names
data["bot_online"] = bool(names)
return data
def build_admin_app() -> FastAPI | None:
"""构建 MC 服务器数据管理 API 子应用(挂载到 /api/mc_server_status)。"""
app = FastAPI(title="MC Server Status API")
auth = require_admin
@app.get("/overview", response_class=JSONResponse, dependencies=[auth])
async def overview():
"""一次拿全:所有群 + 服务器列表 + 类型选项。"""
return _ok(await _payload_with_names())
@app.post("/server", response_class=JSONResponse, dependencies=[auth])
async def save_server(data: dict):
"""新增/更新一台服务器;带 old_name 且与 name 不同即为改名。"""
try:
group_id = store.parse_group_id(data.get("group_id"))
name = store.normalize_name(data.get("name"))
created = store.set_server(
group_id,
name,
str(data.get("address") or ""),
str(data.get("type") or ""),
old_name=str(data.get("old_name") or "").strip() or None,
)
except store.ServerError as e:
return _fail(str(e))
return _ok(
await _payload_with_names(),
f"服务器「{name}」已{'添加' if created else '保存'}",
)
@app.delete("/server/{group_id}/{name}", response_class=JSONResponse, dependencies=[auth])
async def remove_server(group_id: str, name: str):
"""删掉某群里的一台服务器(删空了连带删群)。"""
try:
store.delete_server(group_id, name)
except store.ServerError as e:
return _fail(str(e))
return _ok(await _payload_with_names(), f"服务器「{name}」已删除")
@app.delete("/group/{group_id}", response_class=JSONResponse, dependencies=[auth])
async def remove_group(group_id: str):
"""删掉整个群的服务器配置。"""
try:
store.delete_group(group_id)
except store.ServerError as e:
return _fail(str(e))
return _ok(await _payload_with_names(), f"群 {group_id} 的服务器配置已删除")
@app.post("/test", response_class=JSONResponse, dependencies=[auth])
async def test_server(data: dict):
"""测试连接:给了群号+名字就测存量那台,否则测临时填的地址(不落库)。
对不上的地址走 mcstatus 自己的 2 秒超时,返回的是查询失败文案而不是报错。
"""
group_id = str(data.get("group_id") or "").strip()
name = str(data.get("name") or "").strip()
address = str(data.get("address") or "").strip()
server_type = str(data.get("type") or "").strip()
if group_id.isdigit() and name:
found = store.lookup(int(group_id), name)
if found:
address, server_type = found[0], found[1]
if not address:
return _fail("先填服务器地址")
try:
address = store.normalize_address(address)
server_type = store.normalize_type(server_type)
except store.ServerError as e:
return _fail(str(e))
message = await mc_service.check_mc_status(name or "测试", address, server_type)
# check_mc_status 正常返回 str;带图标的 Java 服返回 Message(Web 侧只要纯文本)
text = str(message) if not isinstance(message, str) else message
return _ok({"ok": "查询失败" not in text, "message": text}, "测试完成")
return app