Add HeXi bot codebase: custom plugins, web frontends, tests

- hexi core: message handling, rate limiting, cooldown, plugin manager
- Custom plugins: BF stats, daily check-in, quotes, persona cards, etc.
- Community plugins vendored under hexi/plugins with local fixes
- Web admin frontends (learning-chat, persona-admin), unified hexi/web
- Tests for rate_limit/cooldown/memes/persona; poetry.lock

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-01 13:13:40 +08:00
co-authored by Claude
parent 1783c60afa
commit b61d09f09f
3201 changed files with 160436 additions and 171 deletions
@@ -0,0 +1,244 @@
import ast
from asyncio import gather
from base64 import b64decode
from io import BytesIO
from re import findall
from typing import Union
import json
from mcstatus import BedrockServer, JavaServer
from nonebot import on_command, on_regex
from nonebot.adapters.onebot.v11 import (
Bot,
GroupMessageEvent,
Message,
MessageEvent,
)
from nonebot.adapters.onebot.v11 import (
MessageSegment as MS,
)
from nonebot.log import logger
from nonebot.params import RegexGroup
from nonebot.plugin import PluginMetadata
from .config import Config, pc, save_file, var
# 统一配置标准:把管理 QQ / 机器人 QQ / 数据文件名暴露给 Web 管理台
from hexi.config_standard import register_model_config # noqa: E402
register_model_config(
__name__,
pc,
fields=["mc_status_admin_qqnum", "mc_status_bot_qqnum_list", "mc_status_data_filename"],
labels={
"mc_status_admin_qqnum": "管理员 QQ 号",
"mc_status_bot_qqnum_list": "响应机器人 QQ 列表",
"mc_status_data_filename": "数据文件名",
},
descriptions={
"mc_status_admin_qqnum": "可执行添加/删除服务器等管理的 QQ 号(每行一个)",
"mc_status_bot_qqnum_list": "按优先级响应的机器人 QQ 号(每行一个,可空)",
"mc_status_data_filename": "群服务器数据文件名(hexi/data 下)",
},
)
__plugin_meta__ = PluginMetadata(
name="MC服务器信息查询插件",
description="如名",
type="application",
homepage="https://github.com/nikissXI/nonebot_plugins/tree/main/nonebot_plugin_setu_customization",
supported_adapters={"~onebot.v11"},
config=Config,
usage=f"""插件命令如下:
信息 # 字面意思,需要加命令前缀,默认/
信息数据 # 查看已启用群以及服务器信息,需要加命令前缀,默认/
添加服务器 # 字面意思
删除服务器 # 字面意思
""",
)
async def group_check(event: GroupMessageEvent, bot: Bot) -> bool:
return event.group_id in var.group_list and bot == var.handle_bot
async def admin_check(event: MessageEvent, bot: Bot) -> bool:
return bot == var.handle_bot and event.user_id in pc.mc_status_admin_qqnum
xinxi = on_command("信息", rule=group_check)
list_all = on_command("信息数据", rule=admin_check)
add_server = on_regex(
r"^添加服务器\s*((\d+)\s+(\S+)\s+(\S+)\s+(\S+))?", rule=admin_check
)
del_server = on_regex(r"^删除服务器\s*((\d+)\s+(\S+))?", rule=admin_check)
test_server = on_regex(r"^测试服务器\s*((\S+)\s+(\S+))?", rule=admin_check)
@xinxi.handle()
async def _(event: GroupMessageEvent):
group = event.group_id
task_list = []
for server_name in var.group_list[group]:
server_host = var.group_list[group][server_name][0]
server_type = var.group_list[group][server_name][1]
task_list.append(
check_mc_status(
server_name,
server_host,
server_type,
)
)
result = await gather(*task_list)
count = 0
msg = ""
print(result)
for r in result:
count += 1
if count > 1:
msg += "\n=== 分割线 ===\n"
msg += r
await xinxi.finish(msg)
@add_server.handle()
async def _(mp=RegexGroup()):
if not mp[0]:
await add_server.finish(
f"添加服务器 [群号] [名称] [服务器地址] [类型]\n类型写js或bds,js是Java服务器,bds是基岩服务器\n服务器地址如果知道端口号把端口加上,否则查询速度会慢一点\n添加例子:\nexp1: 添加服务器 114514 哈皮咳嗽 mc.hypixel.net js\nexp2: 添加服务器 114514 某基岩服 mc.bds.net bds\nexp3: 添加服务器 114514 某Java服 mc.java.net:25577 js"
)
else:
group = int(mp[1])
new_server_name = mp[2]
server_host = mp[3]
server_type = mp[4].lower()
if server_type not in ["js", "bds"]:
await add_server.finish("类型请填js或bds")
if group not in var.group_list:
var.group_list[group] = {new_server_name: [server_host, server_type]}
else:
for server_name in var.group_list[group]:
if new_server_name == server_name:
await add_server.finish("有同名服务器啦!")
var.group_list[group][new_server_name] = [server_host, server_type]
save_file()
await add_server.finish("添加成功")
@del_server.handle()
async def _(mp=RegexGroup()):
if not mp[0]:
await del_server.finish(f"删除服务器 [群号] [名称]")
else:
group = int(mp[1])
name = mp[2]
if group not in var.group_list:
await del_server.finish("这个群没有添加服务器")
else:
if name in var.group_list[group]:
var.group_list[group].pop(name)
if not var.group_list[group]:
var.group_list.pop(group)
save_file()
await del_server.finish("删除成功")
else:
await del_server.finish("没找到该名称的服务器")
@list_all.handle()
async def _():
msg = ""
for group_id in var.group_list:
msg += f"群{group_id}服务器列表\n"
for server_name in var.group_list[group_id]:
server_host, server_type = var.group_list[group_id][server_name]
msg += f"{server_name} {server_host} {server_type}\n"
msg += "\n"
if not msg:
msg = "无数据"
await list_all.finish(f"mc_status数据\n{msg}")
@test_server.handle()
async def _(mp=RegexGroup()):
if not mp[0]:
await test_server.finish(
f"测试服务器 [服务器地址] [类型]\n类型写js或bds,js是Java服务器,bds是基岩服务器"
)
else:
server_host = mp[1]
server_type = mp[2].lower()
if server_type not in ["js", "bds"]:
await add_server.finish("类型请填js或bds")
msg = await check_mc_status("测试", server_host, server_type)
await list_all.finish(msg)
async def check_mc_status(
name: str, host: str, server_type: str
) -> Union[str, Message]:
try:
if server_type == "js":
js = await JavaServer.async_lookup(host, timeout=2)
status = js.status()
# if status.description.strip():
# print(f"des: {status.description}")
version_list = findall(r"\d+\.\d+(?:\.[\dxX]+)?", status.version.name)
if len(version_list) != 1:
version = f"{version_list[0]}-{version_list[-1]}"
else:
version = version_list[0]
online = f"{status.players.online}/{status.players.max}"
if status.players.online and status.players.sample:
anonymous_player = 0
_player_list = []
for p in status.players.sample:
if p.id == "00000000-0000-0000-0000-000000000000":
anonymous_player += 1
else:
_player_list.append(p.name)
if anonymous_player:
_player_list.append(f"[{anonymous_player}个匿名玩家]")
if _player_list:
player_list = "\n".join(_player_list)
else:
player_list = "没返回玩家列表"
else:
player_list = "没人在线"
latency = round(status.latency)
# base64图标
if status.favicon:
aa, bb = status.favicon.split("base64,")
icon = MS.image(BytesIO(b64decode(bb))) + "\n"
else:
icon = ""
msg = (
icon
+ f"名称:{name}\n版本:{version}\n在线:{online}\n延迟:{latency}ms\n在线列表:\n{player_list}"
)
else:
if host.find(":") != -1:
host, port = host.split(":")
else:
host, port = host, 19132
bds = BedrockServer(host=host, port=int(port))
status = await bds.async_status()
online = f"{status.players_online}/{status.players_max}"
latency = round(status.latency)
version = status.version.version
msg = f"名称:{name} 【{version}】\n在线:{online} 延迟:{latency}ms"
except Exception as e:
msg = f"名称:{name} 查询失败!\n错误:{repr(e)}"
return msg
@@ -0,0 +1,115 @@
from json import dump, load
from os import makedirs
from pathlib import Path
from typing import List, Optional
from nonebot import get_bot, get_bots, get_driver, get_plugin_config
from nonebot.adapters import Bot
from pydantic import BaseModel
# 数据目录:项目 hexi/data/ 下(与其它插件数据统一)
DATA_DIR = Path(__file__).resolve().parents[2] / "data"
class Config(BaseModel):
# 管理员的QQ号(别问我为什么)
mc_status_admin_qqnum: List[int] = [] # 必填
# 机器人的QQ号(如果写了就按优先级响应,否则就第一个连上的响应) ['1234','5678','6666']
mc_status_bot_qqnum_list: List[str] = [] # 可选
# 数据文件名
mc_status_data_filename: str = "mc_status_data.json"
class Var:
# 处理消息的bot
handle_bot: Optional[Bot] = None
# {"123456": {"提肛": ["mc.hypixel.net:25565","java"]}}
group_list = {}
driver = get_driver()
pc = get_plugin_config(Config)
var = Var()
@driver.on_startup
async def on_startup():
makedirs(DATA_DIR, exist_ok=True)
if not (DATA_DIR / pc.mc_status_data_filename).exists():
save_file()
else:
load_file()
def load_file():
with open(DATA_DIR / pc.mc_status_data_filename, "r", encoding="utf-8") as r:
tmp_data = load(r)
for i in tmp_data:
var.group_list[int(i)] = tmp_data[i]
def save_file():
with open(DATA_DIR / pc.mc_status_data_filename, "w", encoding="utf-8") as w:
dump(var.group_list, w, indent=4, ensure_ascii=False)
# qq机器人连接时执行
@driver.on_bot_connect
async def on_bot_connect(bot: Bot):
# 是否有写bot qq,如果写了只处理bot qq在列表里的
if pc.mc_status_bot_qqnum_list and bot.self_id in pc.mc_status_bot_qqnum_list:
# 如果已经有bot连了
if var.handle_bot:
# 当前bot qq 下标
handle_bot_id_index = pc.mc_status_bot_qqnum_list.index(
var.handle_bot.self_id
)
# 新连接的bot qq 下标
new_bot_id_index = pc.mc_status_bot_qqnum_list.index(bot.self_id)
# 判断优先级,下标越低优先级越高
if new_bot_id_index < handle_bot_id_index:
var.handle_bot = bot
# 没bot连就直接给
else:
var.handle_bot = bot
# 不写就给第一个连的
elif not pc.mc_status_bot_qqnum_list and not var.handle_bot:
var.handle_bot = bot
# qq机器人断开时执行
@driver.on_bot_disconnect
async def on_bot_disconnect(bot: Bot):
# 判断掉线的是否为handle bot
if bot == var.handle_bot:
# 如果有写bot qq列表
if pc.mc_status_bot_qqnum_list:
# 获取当前连着的bot列表(需要bot是在bot qq列表里)
available_bot_id_list = [
bot_id for bot_id in get_bots() if bot_id in pc.mc_status_bot_qqnum_list
]
if available_bot_id_list:
# 打擂台排序?
new_bot_index = pc.mc_status_bot_qqnum_list.index(
available_bot_id_list[0]
)
for bot_id in available_bot_id_list:
now_bot_index = pc.mc_status_bot_qqnum_list.index(bot_id)
if now_bot_index < new_bot_index:
new_bot_index = now_bot_index
# 取下标在qq列表里最小的bot qq为新的handle bot
var.handle_bot = get_bot(pc.mc_status_bot_qqnum_list[new_bot_index])
else:
var.handle_bot = None
# 不写就随便给一个连着的(如果有)
elif var.handle_bot:
try:
new_bot = get_bot()
var.handle_bot = new_bot
except ValueError:
var.handle_bot = None