zhenxun_bot/zhenxun/utils/manager/bot_profile_manager.py
Rumio 11524bcb04
Some checks failed
检查bot是否运行正常 / bot check (push) Has been cancelled
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
Sequential Lint and Type Check / ruff-call (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Force Sync to Aliyun / sync (push) Has been cancelled
Update Version / update-version (push) Has been cancelled
Sequential Lint and Type Check / pyright-call (push) Has been cancelled
♻️ refactor: 统一图片渲染架构并引入通用UI组件系统 (#2019)
* ♻️ refactor: 统一图片渲染架构并引入通用UI组件系统

🎨 **渲染服务重构**
- 统一图片渲染入口,引入主题系统支持
- 优化Jinja2环境管理,支持主题覆盖和插件命名空间
- 新增UI缓存机制和主题重载功能

 **通用UI组件系统**
- 新增 zhenxun.ui 模块,提供数据模型和构建器
- 引入BaseBuilder基类,支持链式调用
- 新增多种UI构建器:InfoCard, Markdown, Table, Chart, Layout等
- 新增通用组件:Divider, Badge, ProgressBar, UserInfoBlock

🔄 **插件迁移**
- 迁移9个内置插件至新渲染系统
- 移除各插件中分散的图片生成工具
- 优化数据处理和渲染逻辑

💥 **Breaking Changes**
- 移除旧的图片渲染接口和模板路径
- TEMPLATE_PATH 更名为 THEMES_PATH
- 插件需适配新的RendererService和zhenxun.ui模块

*  test(check): 更新自检插件测试中的渲染服务模拟

* ♻️ refactor(renderer): 将缓存文件名哈希算法切换到 SHA256

* ♻️ refactor(shop): 移除商店HTML图片生成模块

* 🚨 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-15 16:34:37 +08:00

170 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.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 import renderer_service
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 renderer_service.render(
"pages/builtin/bot_profile", data=profile_data
)
BotProfileManager.clear_profile_image()