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,81 @@
import asyncio
from cookit.loguru import warning_suppress
from cookit.nonebot.alconna import extract_reply_msg
from nonebot import logger, on_command
from nonebot.adapters import Bot as BaseBot, Event as BaseEvent, Message as BaseMessage
from nonebot.matcher import current_bot, current_event, current_matcher
from nonebot.params import CommandArg
from nonebot.permission import SUPERUSER
from nonebot.rule import Rule, to_me
from nonebot.typing import T_State
from nonebot_plugin_alconna.uniseg import Image, OriginalUniMsg, UniMessage, image_fetch
from .bg_provider import BgBytesData, bg_preloader
from .collectors import collect_all
from .config import config
from .misc_statistics import bot_avatar_cache, bot_info_cache, cache_bot_avatar
from .templates import render_current_template
def check_empty_arg_rule(arg: BaseMessage = CommandArg()):
return not arg.extract_plain_text()
def trigger_rule():
rule = Rule(check_empty_arg_rule)
if config.ps_need_at:
rule &= to_me()
return rule
_cmd, *_alias = config.ps_command
stat_matcher = on_command(
_cmd,
aliases=set(_alias),
rule=trigger_rule(),
permission=SUPERUSER if config.ps_only_su else None,
)
async def get_pic_from_msg(msg: UniMessage) -> BgBytesData | None:
msg = r if ((r := extract_reply_msg(msg)) and Image in r) else msg
if Image not in msg:
return None
img = msg[Image, 0]
data = await image_fetch(
current_event.get(),
current_bot.get(),
current_matcher.get().state,
img,
)
if not data:
return None
return BgBytesData(data=data, mime=img.mimetype or "image")
@stat_matcher.handle()
async def _(bot: BaseBot, event: BaseEvent, state: T_State, msg: OriginalUniMsg):
if (
(bot.self_id not in bot_avatar_cache)
and (info := bot_info_cache.get(bot.self_id))
and info.avatar
):
await cache_bot_avatar(info.avatar, bot, event, state)
async def get_bg():
with warning_suppress("Failed to fetch image from user message"):
if bg := await get_pic_from_msg(msg):
return bg
return await bg_preloader.get()
try:
bg, collected = await asyncio.gather(get_bg(), collect_all())
ret = await render_current_template(collected=collected, bg=bg)
except Exception:
logger.exception("获取运行状态图失败")
await UniMessage("获取运行状态图片失败,请检查后台输出").send(
reply_to=config.ps_reply_target,
)
else:
await UniMessage.image(raw=ret).send(reply_to=config.ps_reply_target)