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,153 @@
from collections.abc import Callable
from datetime import datetime
from typing import Any
from cookit.loguru import log_exception_warning
from nonebot import get_driver, logger
from nonebot.adapters import Bot as BaseBot, Event as BaseEvent
from nonebot.message import event_preprocessor
from nonebot.typing import T_State
from nonebot_plugin_alconna import image_fetch
from nonebot_plugin_alconna.uniseg import Image
from nonebot_plugin_uninfo import User, get_interface
from .config import config
nonebot_run_time: datetime = datetime.now().astimezone()
bot_connect_time: dict[str, datetime] = {}
recv_num: dict[str, int] = {}
send_num: dict[str, int] = {}
bot_info_cache: dict[str, User] = {}
bot_avatar_cache: dict[str, bytes | None] = {}
driver = get_driver()
SEND_APIS: dict[str, list[str] | Callable[[str], bool]] = {
# "BilibiliLive": [], # 狗东西发消息不走 call_api
"Console": ["send_msg"],
"Ding": ["send"],
"Discord": ["create_message"],
"Feishu": ["im/v1/messages"],
"Kaiheila": ["message_create", "directMessage_create"],
"Minecraft": ["send_msg"],
"mirai2": ["send_friend_message", "send_group_message", "send_temp_message"],
"ntchat": lambda x: x.startswith("send_"),
"OneBot V11": ["send_private_msg", "send_group_msg", "send_msg"],
"OneBot V12": ["send_message"],
"QQ": [
"post_dms_messages",
"post_messages",
"post_c2c_messages",
"post_c2c_files",
"post_group_messages",
"post_group_files",
],
"RedProtocol": ["send_message", "send_fake_forward"],
"Satori": ["message_create"],
"Telegram": lambda x: x.startswith("send_"),
"大别野": ["send_message"],
}
def method_is_send_msg(platform: str, name: str) -> bool:
return (platform in SEND_APIS) and (
(name in it) if isinstance((it := SEND_APIS[platform]), list) else it(name)
)
if config.ps_count_message_sent_event:
@event_preprocessor
async def _(bot: BaseBot, event: BaseEvent):
if (
config.ps_count_message_sent_event
and (
(config.ps_count_message_sent_event is True)
or bot.adapter.get_name() in config.ps_count_message_sent_event
)
and (
(event.get_type() == "message_sent")
or (
event.get_type() == "message" and event.get_user_id() == bot.self_id
)
)
):
# logger.debug(f"Bot {bot.self_id} sent counter +1")
send_num[bot.self_id] += 1
if config.ps_count_message_sent_event is not True:
@BaseBot.on_called_api
async def called_api(
bot: BaseBot,
exc: Exception | None,
api: str,
_: dict[str, Any],
__: Any,
):
if (
(not exc)
and (config.ps_count_message_sent_event is not True)
and (
(config.ps_count_message_sent_event is False)
or (bot.adapter.get_name() not in config.ps_count_message_sent_event)
)
and method_is_send_msg(bot.adapter.get_name(), api)
):
# logger.debug(f"Bot {bot.self_id} sent counter +1")
send_num[bot.self_id] += 1
async def cache_bot_avatar(avatar: str, bot: BaseBot, event: BaseEvent, state: T_State):
try:
img = await image_fetch(event, bot, state, Image(url=avatar))
except Exception as e:
log_exception_warning(e, f"Failed to get avatar of bot {bot.self_id}")
return None
else:
if not img:
logger.warning(
f"Cannot get avatar of bot {bot.self_id}"
f" because image_fetch returned None",
)
bot_avatar_cache[bot.self_id] = img
return img
async def cache_bot_info(bot: BaseBot):
try:
it = get_interface(bot)
info = (await it.get_user(bot.self_id)) if it else None
except Exception as e:
logger.warning(f"Error when getting bot info: {e.__class__.__name__}: {e}")
else:
if info:
bot_info_cache[bot.self_id] = info
return info
return None
@driver.on_bot_connect
async def _(bot: BaseBot):
bot_connect_time[bot.self_id] = datetime.now().astimezone()
if bot.self_id not in recv_num:
recv_num[bot.self_id] = 0
if (bot.self_id not in send_num) and (bot.adapter.get_name() in SEND_APIS):
send_num[bot.self_id] = 0
await cache_bot_info(bot)
@driver.on_bot_disconnect
async def _(bot: BaseBot):
bot_connect_time.pop(bot.self_id, None)
if config.ps_disconnect_reset_counter:
recv_num.pop(bot.self_id, None)
send_num.pop(bot.self_id, None)
@event_preprocessor
async def _(bot: BaseBot, event: BaseEvent):
if event.get_type() == "message":
recv_num[bot.self_id] += 1