zhenxun_bot/zhenxun/utils/manager/bot_profile_manager.py
Rumio 6124e217d0
Some checks failed
检查bot是否运行正常 / bot check (push) Waiting to run
Sequential Lint and Type Check / ruff-call (push) Waiting to run
Sequential Lint and Type Check / pyright-call (push) Blocked by required conditions
Release Drafter / Update Release Draft (push) Waiting to run
Force Sync to Aliyun / sync (push) Waiting to run
Update Version / update-version (push) Waiting to run
CodeQL Code Security Analysis / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL Code Security Analysis / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
♻️ refactor(UI): 重构UI渲染服务为组件化分层架构 (#2025)
* ♻️ refactor(UI): 重构UI渲染服务为组件化分层架构

♻️ **架构重构**
- UI渲染服务重构为组件化分层架构
- 解耦主题管理、HTML生成、截图功能

 **新增功能**
- `zhenxun.ui` 统一入口,提供 `render`、`markdown`、`vstack` 等API
- `RenderableComponent` 基类和渲染协议抽象
- 新增主题管理器和截图引擎模块

⚙️ **配置优化**
- UI配置迁移至 `superuser/ui_manager.py`
- 新增"重载UI主题"管理指令

🔧 **性能改进**
- 优化渲染缓存,支持组件级透明缓存
- 所有UI组件适配新渲染流程

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: webjoin111 <455457521@qq.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-08-18 23:08:22 +08:00

172 lines
5.3 KiB
Python

import asyncio
from pathlib import Path
from typing import ClassVar
import aiofiles
import nonebot
from pydantic import BaseModel
from zhenxun import ui
from zhenxun.configs.config import BotConfig, Config
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.configs.utils.models import PluginExtraData
from zhenxun.models.statistics import Statistics
from zhenxun.models.user_console import UserConsole
from zhenxun.services.log import logger
from zhenxun.utils.platform import PlatformUtils
from zhenxun.utils.pydantic_compat import model_dump
DIR_PATH = DATA_PATH / "bot_profile"
PROFILE_PATH = DIR_PATH / "profile"
PROFILE_PATH.mkdir(parents=True, exist_ok=True)
Config.add_plugin_config(
"bot_profile",
"AUTO_SEND_PROFILE",
True,
help="在添加好友/群组时是否自动发送BOT自我介绍图片",
default_value=True,
type=bool,
)
class Profile(BaseModel):
bot_id: str
"""BOT ID"""
introduction: str
"""BOT自我介绍"""
avatar: Path | None
"""BOT头像"""
name: str
"""BOT名称"""
class PluginProfile(BaseModel):
name: str
"""插件名称"""
introduction: str
"""插件自我介绍"""
precautions: list[str] | None = None
"""BOT自我介绍时插件的注意事项"""
class BotProfileManager:
"""BOT自我介绍管理器"""
_bot_data: ClassVar[dict[str, Profile]] = {}
_plugin_data: ClassVar[dict[str, PluginProfile]] = {}
@classmethod
def clear_profile_image(cls, bot_id: str | None = None):
"""清除BOT自我介绍的内存缓存"""
if bot_id:
if bot_id in cls._bot_data:
del cls._bot_data[bot_id]
else:
cls._bot_data.clear()
@classmethod
async def _read_profile(cls, bot_id: str):
"""读取BOT自我介绍
参数:
bot_id: BOT ID
异常:
FileNotFoundError: 文件不存在
"""
bot_file_path = PROFILE_PATH / f"{bot_id}"
bot_file_path.mkdir(parents=True, exist_ok=True)
bot_profile_file = bot_file_path / "profile.txt"
if not bot_profile_file.exists():
logger.debug(f"BOT自我介绍文件不存在: {bot_profile_file}, 跳过读取")
bot_file_path.touch()
return
async with aiofiles.open(bot_profile_file, encoding="utf-8") as f:
introduction = await f.read()
avatar = bot_file_path / f"{bot_id}.png"
if not avatar.exists():
avatar = None
bot = await PlatformUtils.get_user(nonebot.get_bot(bot_id), bot_id)
name = bot.name if bot else "未知"
cls._bot_data[bot_id] = Profile(
bot_id=bot_id, introduction=introduction, avatar=avatar, name=name
)
@classmethod
async def get_bot_profile(cls, bot_id: str) -> Profile | None:
if bot_id not in cls._bot_data:
await cls._read_profile(bot_id)
return cls._bot_data.get(bot_id)
@classmethod
def load_plugin_profile(cls):
"""加载插件自我介绍"""
for plugin in nonebot.get_loaded_plugins():
if plugin.module_name in cls._plugin_data:
continue
metadata = plugin.metadata
if not metadata:
continue
extra = metadata.extra
if not extra:
continue
extra_data = PluginExtraData(**extra)
if extra_data.introduction or extra_data.precautions:
cls._plugin_data[plugin.name] = PluginProfile(
name=metadata.name,
introduction=extra_data.introduction or "",
precautions=extra_data.precautions or [],
)
@classmethod
def get_plugin_profile(cls) -> list[dict]:
"""获取插件自我介绍"""
if not cls._plugin_data:
cls.load_plugin_profile()
return [model_dump(e) for e in cls._plugin_data.values()]
@classmethod
def is_auto_send_profile(cls) -> bool:
"""是否自动发送BOT自我介绍图片"""
return Config.get_config("bot_profile", "AUTO_SEND_PROFILE")
@classmethod
async def build_bot_profile_image(
cls, bot_id: str, tags: list[dict[str, str]] | None = None
) -> bytes | None:
"""构建BOT自我介绍图片"""
profile, service_count, call_count = await asyncio.gather(
cls.get_bot_profile(bot_id),
UserConsole.get_new_uid(),
Statistics.filter(bot_id=bot_id).count(),
)
if not profile:
return None
if not tags:
tags = [
{"text": f"服务人数: {service_count}", "color": "#5e92e0"},
{"text": f"调用次数: {call_count}", "color": "#31e074"},
]
profile_data = {
"avatar": profile.avatar.absolute().as_uri() if profile.avatar else None,
"bot_name": profile.name,
"bot_description": profile.introduction,
"service_count": service_count,
"call_count": call_count,
"plugin_list": cls.get_plugin_profile(),
"tags": tags,
"title": f"{BotConfig.self_nickname}简介",
}
return await ui.render_template(
"pages/builtin/bot_profile",
data=profile_data,
use_cache=True,
)
BotProfileManager.clear_profile_image()