♻️ refactor: 统一图片渲染架构并引入通用UI组件系统 (#2019)
检查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组件系统

🎨 **渲染服务重构**
- 统一图片渲染入口,引入主题系统支持
- 优化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>
This commit is contained in:
Rumio
2025-08-15 16:34:37 +08:00
committed by GitHub
co-authored by webjoin111 pre-commit-ci[bot]
parent d5e5fac02d
commit 11524bcb04
78 changed files with 3130 additions and 2222 deletions
@@ -2,18 +2,14 @@ from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
from nonebot_plugin_session import EventSession
from zhenxun.configs.config import Config
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
from zhenxun.configs.utils import PluginExtraData
from zhenxun.services.help_service import create_plugin_help_image
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType
from zhenxun.utils.exception import EmptyError
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.rules import admin_check, ensure_group
from .config import ADMIN_HELP_IMAGE
from .html_help import build_html_help
from .normal_help import build_help
__plugin_meta__ = PluginMetadata(
name="群组管理员帮助",
description="管理员帮助列表",
@@ -30,17 +26,19 @@ __plugin_meta__ = PluginMetadata(
precautions=[
"只有群主/群管理 才能使用哦,群主拥有6级权限,管理员拥有5级权限!"
],
configs=[
RegisterConfig(
key="type",
value="zhenxun",
help="管理员帮助样式,normal, zhenxun",
default_value="zhenxun",
)
],
configs=[],
).to_dict(),
)
async def build_html_help() -> bytes:
"""构建管理员帮助图片"""
return await create_plugin_help_image(
plugin_types=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN],
page_title="群管理员帮助手册",
)
_matcher = on_alconna(
Alconna("管理员帮助"),
rule=admin_check(1) & ensure_group,
@@ -54,15 +52,9 @@ async def _(
session: EventSession,
arparma: Arparma,
):
if not ADMIN_HELP_IMAGE.exists():
try:
if Config.get_config("admin_help", "type") == "zhenxun":
await build_html_help()
else:
await build_help()
except EmptyError:
await MessageUtils.build_message("当前管理员帮助为空...").finish(
reply_to=True
)
await MessageUtils.build_message(ADMIN_HELP_IMAGE).send()
try:
image_bytes = await build_html_help()
await MessageUtils.build_message(image_bytes).send()
except EmptyError:
await MessageUtils.build_message("当前管理员帮助为空...").finish(reply_to=True)
logger.info("查看管理员帮助", arparma.header_result, session=session)
@@ -1,23 +0,0 @@
from nonebot.plugin import PluginMetadata
from pydantic import BaseModel
from zhenxun.configs.path_config import IMAGE_PATH
from zhenxun.models.plugin_info import PluginInfo
ADMIN_HELP_IMAGE = IMAGE_PATH / "ADMIN_HELP.png"
if ADMIN_HELP_IMAGE.exists():
ADMIN_HELP_IMAGE.unlink()
class PluginData(BaseModel):
"""
插件信息
"""
plugin: PluginInfo
"""插件信息"""
metadata: PluginMetadata
"""元数据"""
class Config:
arbitrary_types_allowed = True
@@ -1,57 +0,0 @@
from nonebot_plugin_htmlrender import template_to_pic
from zhenxun.builtin_plugins.admin.admin_help.config import ADMIN_HELP_IMAGE
from zhenxun.configs.config import BotConfig
from zhenxun.configs.path_config import TEMPLATE_PATH
from zhenxun.models.task_info import TaskInfo
from zhenxun.utils._build_image import BuildImage
from .utils import get_plugins
async def get_task() -> dict[str, str] | None:
"""获取被动技能帮助"""
if task_list := await TaskInfo.all():
return {
"name": "被动技能",
"description": "控制群组中的被动技能状态",
"usage": "通过 开启/关闭群被动 来控制群被动 <br>"
+ " 示例:开启/关闭群被动早晚安 <br> 示例:开启/关闭全部群被动"
+ " <br> ---------- <br> "
+ "<br>".join([task.name for task in task_list]),
}
return None
async def build_html_help():
"""构建帮助图片"""
plugins = await get_plugins()
plugin_list = [
{
"name": data.plugin.name,
"description": data.metadata.description.replace("\n", "<br>"),
"usage": data.metadata.usage.replace("\n", "<br>"),
}
for data in plugins
]
if task := await get_task():
plugin_list.append(task)
plugin_list.sort(key=lambda p: len(p["description"]) + len(p["usage"]))
pic = await template_to_pic(
template_path=str((TEMPLATE_PATH / "help").absolute()),
template_name="main.html",
templates={
"data": {
"plugin_list": plugin_list,
"nickname": BotConfig.self_nickname,
"help_name": "群管理员",
}
},
pages={
"viewport": {"width": 824, "height": 10},
"base_url": f"file://{TEMPLATE_PATH}",
},
wait=2,
)
result = await BuildImage.open(pic).resize(0.5)
await result.save(ADMIN_HELP_IMAGE)
@@ -1,127 +0,0 @@
from nonebot.plugin import PluginMetadata
from PIL.ImageFont import FreeTypeFont
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.models.task_info import TaskInfo
from zhenxun.services.log import logger
from zhenxun.utils._build_image import BuildImage
from zhenxun.utils.image_utils import build_sort_image, group_image, text2image
from .config import ADMIN_HELP_IMAGE
from .utils import get_plugins
async def build_usage_des_image(
metadata: PluginMetadata,
) -> tuple[BuildImage | None, BuildImage | None]:
"""构建用法和描述图片
参数:
metadata: PluginMetadata
返回:
tuple[BuildImage | None, BuildImage | None]: 用法和描述图片
"""
usage = None
description = None
if metadata.usage:
usage = await text2image(
metadata.usage,
padding=5,
color=(255, 255, 255),
font_color=(0, 0, 0),
)
if metadata.description:
description = await text2image(
metadata.description,
padding=5,
color=(255, 255, 255),
font_color=(0, 0, 0),
)
return usage, description
async def build_image(
plugin: PluginInfo, metadata: PluginMetadata, font: FreeTypeFont
) -> BuildImage:
"""构建帮助图片
参数:
plugin: PluginInfo
metadata: PluginMetadata
font: FreeTypeFont
返回:
BuildImage: 帮助图片
"""
usage, description = await build_usage_des_image(metadata)
width = 0
height = 100
if usage:
width = usage.width
height += usage.height
if description and description.width > width:
width = description.width
height += description.height
font_width, _ = BuildImage.get_text_size(f"{plugin.name}[{plugin.level}]", font)
if font_width > width:
width = font_width
A = BuildImage(width + 30, height + 120, "#EAEDF2")
await A.text((15, 10), f"{plugin.name}[{plugin.level}]")
await A.text((15, 70), "简介:")
if not description:
description = BuildImage(A.width - 30, 30, (255, 255, 255))
await description.circle_corner(10)
await A.paste(description, (15, 100))
if not usage:
usage = BuildImage(A.width - 30, 30, (255, 255, 255))
await usage.circle_corner(10)
await A.text((15, description.height + 115), "用法:")
await A.paste(usage, (15, description.height + 145))
await A.circle_corner(10)
return A
async def build_help():
"""构造管理员帮助图片
返回:
BuildImage: 管理员帮助图片
"""
font = BuildImage.load_font("HYWenHei-85W.ttf", 20)
image_list = []
for data in await get_plugins():
plugin = data.plugin
metadata = data.metadata
try:
A = await build_image(plugin, metadata, font)
image_list.append(A)
except Exception as e:
logger.warning(
f"获取群管理员插件 {plugin.module}: {plugin.name} 设置失败...",
"管理员帮助",
e=e,
)
if task_list := await TaskInfo.all():
task_str = "\n".join([task.name for task in task_list])
task_str = "通过 开启/关闭群被动 来控制群被动\n----------\n" + task_str
task_image = await text2image(task_str, padding=5, color=(255, 255, 255))
await task_image.circle_corner(10)
A = BuildImage(task_image.width + 50, task_image.height + 85, "#EAEDF2")
await A.text((25, 10), "被动技能")
await A.paste(task_image, (25, 50))
await A.circle_corner(10)
image_list.append(A)
image_group, _ = group_image(image_list)
A = await build_sort_image(image_group, color=(255, 255, 255), padding_top=160)
text = await BuildImage.build_text_image(
"群管理员帮助",
size=40,
)
tip = await BuildImage.build_text_image(
"注: ‘*’ 代表可有多个相同参数 ‘?’ 代表可省略该参数", size=25, font_color="red"
)
await A.paste(text, (50, 30))
await A.paste(tip, (50, 90))
await A.save(ADMIN_HELP_IMAGE)
@@ -1,22 +0,0 @@
import nonebot
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.utils.enum import PluginType
from zhenxun.utils.exception import EmptyError
from .config import PluginData
async def get_plugins() -> list[PluginData]:
"""获取插件数据"""
plugin_list = await PluginInfo.filter(
plugin_type__in=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN]
).all()
data_list = []
for plugin in plugin_list:
if _plugin := nonebot.get_plugin_by_module_name(plugin.module_path):
if _plugin.metadata:
data_list.append(PluginData(plugin=plugin, metadata=_plugin.metadata))
if not data_list:
raise EmptyError()
return data_list