♻️ 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
+11 -28
View File
@@ -13,11 +13,6 @@ from nonebot_plugin_alconna import (
)
from nonebot_plugin_uninfo import Uninfo
from zhenxun.builtin_plugins.help._config import (
GROUP_HELP_PATH,
SIMPLE_DETAIL_HELP_IMAGE,
SIMPLE_HELP_IMAGE,
)
from zhenxun.configs.config import Config
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
from zhenxun.services.log import logger
@@ -36,18 +31,6 @@ __plugin_meta__ = PluginMetadata(
plugin_type=PluginType.DEPENDANT,
is_show=False,
configs=[
RegisterConfig(
key="type",
value="zhenxun",
help="帮助图片样式 [normal, HTML, zhenxun]",
default_value="zhenxun",
),
RegisterConfig(
key="detail_type",
value="zhenxun",
help="帮助详情图片样式 ['normal', 'zhenxun']",
default_value="zhenxun",
),
RegisterConfig(
key="ENABLE_LLM_HELPER",
value=False,
@@ -76,6 +59,13 @@ __plugin_meta__ = PluginMetadata(
default_value=100,
type=int,
),
RegisterConfig(
key="HELP_STYLE",
value="default",
help="帮助页面的显示样式 (可选值: 'default', 'simple')",
default_value="default",
type=str,
),
],
).to_dict(),
)
@@ -144,15 +134,8 @@ async def _(
f"查看帮助详情失败,未找到: {name.result}", "帮助", session=session
)
elif session.group and (gid := session.group.id):
_image_path = GROUP_HELP_PATH / f"{gid}_{is_detail.result}.png"
if not _image_path.exists():
await create_help_img(session, gid, is_detail.result)
await MessageUtils.build_message(_image_path).finish()
image_bytes = await create_help_img(session, gid, is_detail.result)
await MessageUtils.build_message(image_bytes).finish()
else:
if is_detail.result:
_image_path = SIMPLE_DETAIL_HELP_IMAGE
else:
_image_path = SIMPLE_HELP_IMAGE
if not _image_path.exists():
await create_help_img(session, None, is_detail.result)
await MessageUtils.build_message(_image_path).finish()
image_bytes = await create_help_img(session, None, is_detail.result)
await MessageUtils.build_message(image_bytes).finish()
+135 -124
View File
@@ -1,13 +1,11 @@
from pathlib import Path
import nonebot
from nonebot.plugin import PluginMetadata
from nonebot_plugin_htmlrender import template_to_pic
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.config import Config
from zhenxun.configs.path_config import IMAGE_PATH, TEMPLATE_PATH
from zhenxun.configs.config import BotConfig, Config
from zhenxun.configs.path_config import IMAGE_PATH
from zhenxun.configs.utils import PluginExtraData
from zhenxun.models.bot_console import BotConsole
from zhenxun.models.group_console import GroupConsole
from zhenxun.models.level_user import LevelUser
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.models.statistics import Statistics
@@ -15,60 +13,114 @@ from zhenxun.services import (
LLMException,
LLMMessage,
generate,
renderer_service,
)
from zhenxun.services.log import logger
from zhenxun.utils._image_template import Markdown
from zhenxun.utils.enum import PluginType
from zhenxun.utils.image_utils import BuildImage, ImageTemplate
from ._config import (
GROUP_HELP_PATH,
SIMPLE_DETAIL_HELP_IMAGE,
SIMPLE_HELP_IMAGE,
base_config,
from zhenxun.ui import (
InfoCardBuilder,
NotebookBuilder,
PluginMenuBuilder,
PluginMenuCategory,
)
from .html_help import build_html_image
from .normal_help import build_normal_image
from .zhenxun_help import build_zhenxun_image
from zhenxun.utils.common_utils import format_usage_for_markdown
from zhenxun.utils.enum import BlockType, PluginType
from zhenxun.utils.platform import PlatformUtils
from zhenxun.utils.pydantic_compat import model_dump
from ._utils import classify_plugin
random_bk_path = IMAGE_PATH / "background" / "help" / "simple_help"
background = IMAGE_PATH / "background" / "0.png"
driver = nonebot.get_driver()
def _create_plugin_menu_item(
bot: BotConsole | None,
plugin: PluginInfo,
group: GroupConsole | None,
is_detail: bool,
) -> dict:
"""为插件菜单构造一个插件菜单项数据字典"""
status = True
has_superuser_help = False
nb_plugin = nonebot.get_plugin_by_module_name(plugin.module_path)
if nb_plugin and nb_plugin.metadata and nb_plugin.metadata.extra:
extra_data = PluginExtraData(**nb_plugin.metadata.extra)
if extra_data.superuser_help:
has_superuser_help = True
if not plugin.status:
if plugin.block_type == BlockType.ALL:
status = False
elif group and plugin.block_type == BlockType.GROUP:
status = False
elif not group and plugin.block_type == BlockType.PRIVATE:
status = False
elif group and f"{plugin.module}," in group.block_plugin:
status = False
elif bot and f"{plugin.module}," in bot.block_plugins:
status = False
commands = []
if is_detail and nb_plugin and nb_plugin.metadata and nb_plugin.metadata.extra:
extra_data = PluginExtraData(**nb_plugin.metadata.extra)
commands = [cmd.command for cmd in extra_data.commands]
return {
"id": str(plugin.id),
"name": plugin.name,
"status": status,
"has_superuser_help": has_superuser_help,
"commands": commands,
}
async def create_help_img(
session: Uninfo, group_id: str | None, is_detail: bool
) -> Path:
"""生成帮助图片
) -> bytes:
"""使用渲染服务生成帮助图片"""
classified_data = await classify_plugin(
session, group_id, is_detail, _create_plugin_menu_item
)
参数:
session: Uninfo
group_id: 群号
"""
help_type = base_config.get("type", "").strip().lower()
sorted_categories = dict(
sorted(classified_data.items(), key=lambda x: len(x[1]), reverse=True)
)
categories_for_model = []
plugin_count = 0
active_count = 0
match help_type:
case "html":
result = BuildImage.open(
await build_html_image(session, group_id, is_detail)
)
case "zhenxun":
result = BuildImage.open(
await build_zhenxun_image(session, group_id, is_detail)
)
case _:
result = await build_normal_image(group_id, is_detail)
if group_id:
save_path = GROUP_HELP_PATH / f"{group_id}_{is_detail}.png"
elif is_detail:
save_path = SIMPLE_DETAIL_HELP_IMAGE
else:
save_path = SIMPLE_HELP_IMAGE
await result.save(save_path)
return save_path
if sorted_categories:
menu_key = next(iter(sorted_categories.keys()))
max_data = sorted_categories.pop(menu_key)
main_category_name = "主要功能" if menu_key in ["normal", "功能"] else menu_key
categories_for_model.append({"name": main_category_name, "items": max_data})
plugin_count += len(max_data)
active_count += sum(1 for item in max_data if item["status"])
for menu, value in sorted_categories.items():
category_name = "主要功能" if menu in ["normal", "功能"] else menu
categories_for_model.append({"name": category_name, "items": value})
plugin_count += len(value)
active_count += sum(1 for item in value if item["status"])
platform = PlatformUtils.get_platform(session)
bot_id = BotConfig.get_qbot_uid(session.self_id) or session.self_id
bot_avatar_url = PlatformUtils.get_user_avatar_url(bot_id, platform) or ""
builder = PluginMenuBuilder(
bot_name=BotConfig.self_nickname,
bot_avatar_url=bot_avatar_url,
is_detail=is_detail,
)
for category in categories_for_model:
builder.add_category(
PluginMenuCategory(name=category["name"], items=category["items"])
)
return await builder.build()
async def get_user_allow_help(user_id: str) -> list[PluginType]:
@@ -92,36 +144,6 @@ async def get_user_allow_help(user_id: str) -> list[PluginType]:
return type_list
async def get_normal_help(
metadata: PluginMetadata, extra: PluginExtraData, is_superuser: bool
) -> str | bytes:
"""构建默认帮助详情
参数:
metadata: PluginMetadata
extra: PluginExtraData
is_superuser: 是否超级用户帮助
返回:
str | bytes: 返回信息
"""
items = None
if is_superuser:
if usage := extra.superuser_help:
items = {
"简介": metadata.description,
"用法": usage,
}
else:
items = {
"简介": metadata.description,
"用法": metadata.usage,
}
if items:
return (await ImageTemplate.hl_page(metadata.name, items)).pic2bytes()
return "该功能没有帮助信息"
def min_leading_spaces(str_list: list[str]) -> int:
min_spaces = 9999
@@ -142,45 +164,6 @@ def split_text(text: str):
return [s.replace(" ", "&nbsp;") for s in split_text]
async def get_zhenxun_help(
module: str, metadata: PluginMetadata, extra: PluginExtraData, is_superuser: bool
) -> str | bytes:
"""构建ZhenXun帮助详情
参数:
module: 模块名
metadata: PluginMetadata
extra: PluginExtraData
is_superuser: 是否超级用户帮助
返回:
str | bytes: 返回信息
"""
call_count = await Statistics.filter(plugin_name=module).count()
usage = metadata.usage
if is_superuser:
if not extra.superuser_help:
return "该功能没有超级用户帮助信息"
usage = extra.superuser_help
return await template_to_pic(
template_path=str((TEMPLATE_PATH / "help_detail").absolute()),
template_name="main.html",
templates={
"title": metadata.name,
"author": extra.author,
"version": extra.version,
"call_count": call_count,
"descriptions": split_text(metadata.description),
"usages": split_text(usage),
},
pages={
"viewport": {"width": 824, "height": 590},
"base_url": f"file://{TEMPLATE_PATH}",
},
wait=2,
)
async def get_plugin_help(user_id: str, name: str, is_superuser: bool) -> str | bytes:
"""获取功能的帮助信息
@@ -196,16 +179,42 @@ async def get_plugin_help(user_id: str, name: str, is_superuser: bool) -> str |
plugin = await PluginInfo.get_or_none(
name__iexact=name, load_status=True, plugin_type__in=type_list
)
if plugin:
_plugin = nonebot.get_plugin_by_module_name(plugin.module_path)
if _plugin and _plugin.metadata:
extra_data = PluginExtraData(**_plugin.metadata.extra)
if Config.get_config("help", "detail_type") == "zhenxun":
return await get_zhenxun_help(
plugin.module, _plugin.metadata, extra_data, is_superuser
)
else:
return await get_normal_help(_plugin.metadata, extra_data, is_superuser)
call_count = await Statistics.filter(plugin_name=plugin.module).count()
usage = _plugin.metadata.usage
if is_superuser:
if not extra_data.superuser_help:
return "该功能没有超级用户帮助信息"
usage = extra_data.superuser_help
builder = InfoCardBuilder(title=_plugin.metadata.name)
builder.add_metadata_items(
[
("作者", extra_data.author or "未知"),
("版本", extra_data.version or "未知"),
("调用次数", call_count),
]
)
processed_description = format_usage_for_markdown(
_plugin.metadata.description.strip()
)
processed_usage = format_usage_for_markdown(usage.strip())
builder.add_section("简介", [processed_description])
builder.add_section("使用方法", [processed_usage])
style_name = Config.get_config("help", "HELP_STYLE", "default")
render_dict = model_dump(builder._data)
render_dict["style_name"] = style_name
return await renderer_service.render("pages/builtin/help", data=render_dict)
return "糟糕! 该功能没有帮助喔..."
return "没有查找到这个功能噢..."
@@ -282,10 +291,12 @@ async def get_llm_help(question: str, user_id: str) -> str | bytes:
reply_text = response.text if response else "抱歉,我暂时无法回答这个问题。"
threshold = Config.get_config("help", "LLM_HELPER_REPLY_AS_IMAGE_THRESHOLD", 50)
if len(reply_text) > threshold:
markdown = Markdown()
markdown.text(reply_text)
return await markdown.build()
builder = NotebookBuilder()
builder.text(reply_text)
return await builder.build()
return reply_text
except LLMException as e:
+1 -1
View File
@@ -53,5 +53,5 @@ async def classify_plugin(
classify[menu] = []
classify[menu].append(handle(bot, plugin, group, is_detail))
for value in classify.values():
value.sort(key=lambda x: x.id)
value.sort(key=lambda x: int(x["id"]))
return classify
-150
View File
@@ -1,150 +0,0 @@
import os
import random
from nonebot_plugin_htmlrender import template_to_pic
from nonebot_plugin_uninfo import Uninfo
from pydantic import BaseModel
from zhenxun.configs.path_config import TEMPLATE_PATH
from zhenxun.models.bot_console import BotConsole
from zhenxun.models.group_console import GroupConsole
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.utils.enum import BlockType
from ._utils import classify_plugin
LOGO_PATH = TEMPLATE_PATH / "menu" / "res" / "logo"
class Item(BaseModel):
plugin_name: str
"""插件名称"""
sta: int
"""插件状态"""
id: int
"""插件id"""
class PluginList(BaseModel):
plugin_type: str
"""菜单名称"""
icon: str
"""图标"""
logo: str
"""logo"""
items: list[Item]
"""插件列表"""
ICON2STR = {
"normal": "fa fa-cog",
"原神相关": "fa fa-circle-o",
"常规插件": "fa fa-cubes",
"联系管理员": "fa fa-envelope-o",
"抽卡相关": "fa fa-credit-card-alt",
"来点好康的": "fa fa-picture-o",
"数据统计": "fa fa-bar-chart",
"一些工具": "fa fa-shopping-cart",
"商店": "fa fa-shopping-cart",
"其它": "fa fa-tags",
"群内小游戏": "fa fa-gamepad",
}
def __handle_item(
bot: BotConsole, plugin: PluginInfo, group: GroupConsole | None, is_detail: bool
) -> Item:
"""构造Item
参数:
bot: BotConsole
plugin: PluginInfo
group: 群组
is_detail: 是否详细
返回:
Item: Item
"""
sta = 0
if not plugin.status:
if group and plugin.block_type in [
BlockType.ALL,
BlockType.GROUP,
]:
sta = 2
if not group and plugin.block_type in [
BlockType.ALL,
BlockType.PRIVATE,
]:
sta = 2
if group:
if f"{plugin.module}," in group.superuser_block_plugin:
sta = 2
if f"{plugin.module}," in group.block_plugin:
sta = 1
if bot and f"{plugin.module}," in bot.block_plugins:
sta = 2
return Item(plugin_name=plugin.name, sta=sta, id=plugin.id)
def build_plugin_data(classify: dict[str, list[Item]]) -> list[dict[str, str]]:
"""构建前端插件数据
参数:
classify: 插件数据
返回:
list[dict[str, str]]: 前端插件数据
"""
lengths = [len(classify[c]) for c in classify]
index = lengths.index(max(lengths))
menu_key = list(classify.keys())[index]
max_data = classify[menu_key]
del classify[menu_key]
plugin_list = []
for menu_type in classify:
icon = "fa fa-pencil-square-o"
if menu_type in ICON2STR.keys():
icon = ICON2STR[menu_type]
logo = LOGO_PATH / random.choice(os.listdir(LOGO_PATH))
data = {
"name": menu_type if menu_type != "normal" else "功能",
"items": classify[menu_type],
"icon": icon,
"logo": str(logo.absolute()),
}
plugin_list.append(data)
plugin_list.insert(
0,
{
"name": menu_key if menu_key != "normal" else "功能",
"items": max_data,
"icon": "fa fa-pencil-square-o",
"logo": str((LOGO_PATH / random.choice(os.listdir(LOGO_PATH))).absolute()),
},
)
return plugin_list
async def build_html_image(
session: Uninfo, group_id: str | None, is_detail: bool
) -> bytes:
"""构造HTML帮助图片
参数:
session: Uninfo
group_id: 群号
is_detail: 是否详细帮助
"""
classify = await classify_plugin(session, group_id, is_detail, __handle_item)
plugin_list = build_plugin_data(classify)
return await template_to_pic(
template_path=str((TEMPLATE_PATH / "menu").absolute()),
template_name="zhenxun_menu.html",
templates={"plugin_list": plugin_list},
pages={
"viewport": {"width": 1903, "height": 10},
"base_url": f"file://{TEMPLATE_PATH}",
},
wait=2,
)
-100
View File
@@ -1,100 +0,0 @@
from zhenxun.configs.path_config import IMAGE_PATH
from zhenxun.models.group_console import GroupConsole
from zhenxun.utils._build_image import BuildImage
from zhenxun.utils.enum import BlockType
from zhenxun.utils.image_utils import build_sort_image, group_image
from ._utils import sort_type
BACKGROUND_PATH = IMAGE_PATH / "background" / "help" / "simple_help"
async def build_normal_image(group_id: str | None, is_detail: bool) -> BuildImage:
"""构造PIL帮助图片
参数:
group_id: 群号
is_detail: 详细帮助
"""
image_list = []
font_size = 24
font = BuildImage.load_font("HYWenHei-85W.ttf", 20)
sort_data = await sort_type()
for idx, menu_type in enumerate(sort_data):
plugin_list = sort_data[menu_type]
"""拿到最大宽度和结算高度"""
wh_list = [
BuildImage.get_text_size(f"{x.id}.{x.name}", font) for x in plugin_list
]
wh_list.append(BuildImage.get_text_size(menu_type, font))
sum_height = (font_size + 6) * len(plugin_list) + 10
max_width = max(x[0] for x in wh_list) + 30
bk = BuildImage(
max_width + 40,
sum_height + 50,
font_size=30,
color="#a7d1fc",
font="CJGaoDeGuo.otf",
)
title_size = bk.getsize(menu_type)
max_width = max_width if max_width > title_size[0] else title_size[0]
row = BuildImage(
max_width + 40,
sum_height,
font_size=font_size,
color="black" if idx % 2 else "white",
)
curr_h = 10
group = await GroupConsole.get_group(group_id=group_id) if group_id else None
for _, plugin in enumerate(plugin_list):
text_color = (255, 255, 255) if idx % 2 else (0, 0, 0)
if group and f"{plugin.module}," in group.block_plugin:
text_color = (252, 75, 13)
pos = None
# 禁用状态划线
if plugin.block_type in [BlockType.ALL, BlockType.GROUP] or (
group and f"super:{plugin.module}," in group.block_plugin
):
w = curr_h + int(row.getsize(plugin.name)[1] / 2) + 2
line_width = row.getsize(plugin.name)[0] + 35
pos = (7, w, line_width, w)
await row.text((10, curr_h), f"{plugin.id}.{plugin.name}", text_color)
if pos:
await row.line(pos, (236, 66, 7), 3)
curr_h += font_size + 5
await bk.text((0, 14), menu_type, center_type="width")
await bk.paste(row, (0, 50))
await bk.transparent(2)
image_list.append(bk)
image_group, h = group_image(image_list)
async def _a(image: BuildImage):
await image.filter("GaussianBlur", 5)
result = await build_sort_image(
image_group,
h,
background_path=BACKGROUND_PATH,
background_handle=_a,
)
width, height = 10, 10
for s in [
"目前支持的功能列表:",
"可以通过 '帮助 [功能名称或功能Id]' 来获取对应功能的使用方法",
]:
text = await BuildImage.build_text_image(s, "HYWenHei-85W.ttf", 24)
await result.paste(text, (width, height))
height += 50
if s == "目前支持的功能列表:":
width += 50
text = await BuildImage.build_text_image(
"注: 红字代表功能被群管理员禁用,红线代表功能正在维护",
"HYWenHei-85W.ttf",
24,
(231, 74, 57),
)
await result.paste(
text,
(300, 10),
)
return result
@@ -1,143 +0,0 @@
import nonebot
from nonebot_plugin_htmlrender import template_to_pic
from nonebot_plugin_uninfo import Uninfo
from pydantic import BaseModel
from zhenxun.configs.config import BotConfig
from zhenxun.configs.path_config import TEMPLATE_PATH
from zhenxun.configs.utils import PluginExtraData
from zhenxun.models.bot_console import BotConsole
from zhenxun.models.group_console import GroupConsole
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.utils.enum import BlockType
from zhenxun.utils.platform import PlatformUtils
from ._utils import classify_plugin
class Item(BaseModel):
plugin_name: str
"""插件名称"""
commands: list[str]
"""插件命令"""
id: str
"""插件id"""
status: bool
"""插件状态"""
has_superuser_help: bool
"""插件是否拥有超级用户帮助"""
def __handle_item(
bot: BotConsole | None,
plugin: PluginInfo,
group: GroupConsole | None,
is_detail: bool,
):
"""构造Item
参数:
bot: BotConsole
plugin: PluginInfo
group: 群组
is_detail: 是否为详细
返回:
Item: Item
"""
status = True
has_superuser_help = False
nb_plugin = nonebot.get_plugin_by_module_name(plugin.module_path)
if nb_plugin and nb_plugin.metadata and nb_plugin.metadata.extra:
extra_data = PluginExtraData(**nb_plugin.metadata.extra)
if extra_data.superuser_help:
has_superuser_help = True
if not plugin.status:
if plugin.block_type == BlockType.ALL:
status = False
elif group and plugin.block_type == BlockType.GROUP:
status = False
elif not group and plugin.block_type == BlockType.PRIVATE:
status = False
elif group and f"{plugin.module}," in group.block_plugin:
status = False
elif bot and f"{plugin.module}," in bot.block_plugins:
status = False
commands = []
nb_plugin = nonebot.get_plugin_by_module_name(plugin.module_path)
if is_detail and nb_plugin and nb_plugin.metadata and nb_plugin.metadata.extra:
extra_data = PluginExtraData(**nb_plugin.metadata.extra)
commands = [cmd.command for cmd in extra_data.commands]
return Item(
plugin_name=plugin.name,
commands=commands,
id=str(plugin.id),
status=status,
has_superuser_help=has_superuser_help,
)
def build_plugin_data(classify: dict[str, list[Item]]) -> list[dict[str, str]]:
"""构建前端插件数据
参数:
classify: 插件数据
返回:
list[dict[str, str]]: 前端插件数据
"""
classify = dict(sorted(classify.items(), key=lambda x: len(x[1]), reverse=True))
menu_key = next(iter(classify.keys()))
max_data = classify[menu_key]
del classify[menu_key]
plugin_list = [
{
"name": "主要功能" if menu in ["normal", "功能"] else menu,
"items": value,
}
for menu, value in classify.items()
]
plugin_list.insert(0, {"name": menu_key, "items": max_data})
for plugin in plugin_list:
plugin["items"].sort(key=lambda x: x.id)
return plugin_list
async def build_zhenxun_image(
session: Uninfo, group_id: str | None, is_detail: bool
) -> bytes:
"""构造真寻帮助图片
参数:
bot_id: bot_id
group_id: 群号
is_detail: 是否详细帮助
"""
classify = await classify_plugin(session, group_id, is_detail, __handle_item)
plugin_list = build_plugin_data(classify)
platform = PlatformUtils.get_platform(session)
bot_id = BotConfig.get_qbot_uid(session.self_id) or session.self_id
bot_ava = PlatformUtils.get_user_avatar_url(bot_id, platform)
width = int(637 * 1.5) if is_detail else 637
title_font = int(53 * 1.5) if is_detail else 53
tip_font = int(19 * 1.5) if is_detail else 19
plugin_count = sum(len(plugin["items"]) for plugin in plugin_list)
return await template_to_pic(
template_path=str((TEMPLATE_PATH / "ss_menu").absolute()),
template_name="main.html",
templates={
"data": {
"plugin_list": plugin_list,
"ava": bot_ava,
"width": width,
"font_size": (title_font, tip_font),
"is_detail": is_detail,
"plugin_count": plugin_count,
}
},
pages={
"viewport": {"width": width, "height": 10},
"base_url": f"file://{TEMPLATE_PATH}",
},
wait=2,
)