- 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>
160 lines
6.2 KiB
Python
160 lines
6.2 KiB
Python
"""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
|