"""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