mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-08 21:30:01 +08:00
♻️ refactor(ui): 重构 UI 渲染系统并优化资源管理 (#2094)
* ♻️ refactor(ui): 重构 UI 渲染系统并优化资源管理 - 【重构】重构 `RendererService` 架构,解耦模板引擎、主题管理与截图引擎 - 【重构】重构 `ui` 模块,采用组件注册机制与数据模型驱动,移除旧版 `builders` - 【功能】新增 UI 热重载模式,支持在不重启的情况下实时预览 HTML/CSS 修改 - 【功能】增强启动项资源检查,支持基于 `resources.spec` 的版本校验与自动更新 - 【优化】统一内置插件的 UI 渲染逻辑,迁移至新的 `ui.table`、`ui.markdown` 等工厂接口 - 【优化】优化日志脱敏工具,支持自动折叠调试输出中冗长的样式标签 - 【优化】引入 `AssetResolutionService`,完善皮肤、组件、主题间的多级资源回退机制 - 【优化】新增组件生命周期钩子 `prepare`,支持渲染前的异步数据预处理 * 🚨 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:
co-authored by
webjoin111
pre-commit-ci[bot]
parent
bc8e1659ae
commit
5e30694663
@@ -1,9 +1,12 @@
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import uuid
|
||||
|
||||
import nonebot
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.drivers import Driver
|
||||
from packaging.specifiers import SpecifierSet
|
||||
from packaging.version import Version
|
||||
from tortoise import Tortoise
|
||||
from tortoise.exceptions import IntegrityError, OperationalError
|
||||
import ujson as json
|
||||
@@ -85,8 +88,54 @@ from bag_users t1
|
||||
|
||||
@PriorityLifecycle.on_startup(priority=5)
|
||||
async def _():
|
||||
if not ZhenxunRepoManager.check_resources_exists():
|
||||
await ZhenxunRepoManager.resources_update()
|
||||
try:
|
||||
should_update = False
|
||||
resource_path = ZhenxunRepoManager.config.RESOURCE_PATH
|
||||
default_theme_path = resource_path / "themes" / "default"
|
||||
version_file = resource_path / "__version__"
|
||||
|
||||
if (
|
||||
not ZhenxunRepoManager.check_resources_exists()
|
||||
or not default_theme_path.exists()
|
||||
or not version_file.exists()
|
||||
):
|
||||
should_update = True
|
||||
logger.info(
|
||||
"检测到资源文件(字体/主题/版本信息)缺失,准备进行初始化下载...",
|
||||
"资源检查",
|
||||
)
|
||||
else:
|
||||
spec_file = Path("resources.spec")
|
||||
req_ver_str = ">=0.0.0"
|
||||
if spec_file.exists():
|
||||
try:
|
||||
for line in spec_file.read_text("utf-8").splitlines():
|
||||
if line.strip().startswith("require_resources_version:"):
|
||||
req_ver_str = line.split(":", 1)[1].strip().strip("'\"")
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
local_ver_str = "0.0.0"
|
||||
try:
|
||||
content = version_file.read_text("utf-8").strip()
|
||||
local_ver_str = (
|
||||
content.split(":", 1)[1].strip() if ":" in content else content
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not SpecifierSet(req_ver_str).contains(Version(local_ver_str)):
|
||||
should_update = True
|
||||
logger.info(
|
||||
f"资源版本({local_ver_str})不满足要求({req_ver_str}),准备强制更新...",
|
||||
"资源检查",
|
||||
)
|
||||
|
||||
if should_update:
|
||||
await ZhenxunRepoManager.resources_update()
|
||||
except Exception as e:
|
||||
logger.error(f"资源检查或更新失败: {e}", "资源检查")
|
||||
"""签到与用户的数据迁移"""
|
||||
if goods_list := await GoodsInfo.filter(uuid__isnull=True).all():
|
||||
for goods in goods_list:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import cast
|
||||
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import (
|
||||
@@ -21,7 +22,6 @@ from zhenxun.models.chat_history import ChatHistory
|
||||
from zhenxun.models.group_member_info import GroupInfoUser
|
||||
from zhenxun.services import avatar_service
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.ui.builders import TableBuilder
|
||||
from zhenxun.ui.models import ImageCell, TextCell
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
@@ -121,9 +121,12 @@ async def _(
|
||||
if not show_quit_member:
|
||||
fetch_count = count.result * 2
|
||||
|
||||
if rank_data := await ChatHistory.get_group_msg_rank(
|
||||
raw_rank_data = await ChatHistory.get_group_msg_rank(
|
||||
group_id, fetch_count, "DES" if arparma.find("des") else "DESC", date_scope
|
||||
):
|
||||
)
|
||||
|
||||
if raw_rank_data:
|
||||
rank_data = cast(list[tuple[str, int]], raw_rank_data)
|
||||
rows_data = []
|
||||
platform = "qq"
|
||||
|
||||
@@ -174,10 +177,10 @@ async def _(
|
||||
f"{date_scope[1].replace(microsecond=0)}"
|
||||
)
|
||||
|
||||
builder = TableBuilder(f"消息排行({count.result})", date_str)
|
||||
builder.set_headers(column_name).add_rows(rows_data)
|
||||
table = ui.table(f"消息排行({count.result})", date_str)
|
||||
table.set_headers(column_name).add_rows(rows_data)
|
||||
|
||||
image_bytes = await ui.render(builder.build())
|
||||
image_bytes = await ui.render(table)
|
||||
|
||||
logger.info(
|
||||
f"查看消息排行 数量={count.result}", arparma.header_result, session=session
|
||||
|
||||
@@ -17,11 +17,7 @@ from zhenxun.services import (
|
||||
generate,
|
||||
)
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.ui.builders import (
|
||||
NotebookBuilder,
|
||||
PluginMenuBuilder,
|
||||
)
|
||||
from zhenxun.ui.models import PluginMenuCategory
|
||||
from zhenxun.ui.models import PluginMenuCategory, PluginMenuData
|
||||
from zhenxun.utils.common_utils import format_usage_for_markdown
|
||||
from zhenxun.utils.enum import BlockType, PluginType
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
@@ -109,18 +105,23 @@ async def create_help_img(
|
||||
bot_avatar_path = await avatar_service.get_avatar_path(platform, bot_id)
|
||||
bot_avatar_url = bot_avatar_path.as_uri() if bot_avatar_path else ""
|
||||
|
||||
builder = PluginMenuBuilder(
|
||||
bot_name=BotConfig.self_nickname,
|
||||
bot_avatar_url=bot_avatar_url,
|
||||
is_detail=is_detail,
|
||||
)
|
||||
|
||||
categories_objects = []
|
||||
for category in categories_for_model:
|
||||
builder.add_category(
|
||||
categories_objects.append(
|
||||
PluginMenuCategory(name=category["name"], items=category["items"])
|
||||
)
|
||||
|
||||
return await ui.render(builder.build())
|
||||
# 直接实例化 Data Model
|
||||
menu_data = PluginMenuData(
|
||||
bot_name=BotConfig.self_nickname,
|
||||
bot_avatar_url=bot_avatar_url,
|
||||
is_detail=is_detail,
|
||||
plugin_count=plugin_count,
|
||||
active_count=active_count,
|
||||
categories=categories_objects,
|
||||
)
|
||||
|
||||
return await ui.render(menu_data)
|
||||
|
||||
|
||||
async def get_user_allow_help(user_id: str) -> list[PluginType]:
|
||||
@@ -299,9 +300,9 @@ async def get_llm_help(question: str, user_id: str) -> str | bytes:
|
||||
threshold = Config.get_config("help", "LLM_HELPER_REPLY_AS_IMAGE_THRESHOLD", 50)
|
||||
|
||||
if len(reply_text) > threshold:
|
||||
builder = NotebookBuilder()
|
||||
builder.text(reply_text)
|
||||
return await ui.render(builder.build())
|
||||
notebook = ui.notebook()
|
||||
notebook.text(reply_text)
|
||||
return await ui.render(notebook)
|
||||
|
||||
return reply_text
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from typing import Any
|
||||
|
||||
from zhenxun import ui
|
||||
from zhenxun.services import renderer_service
|
||||
from zhenxun.services.llm.core import KeyStatus
|
||||
from zhenxun.services.llm.types import ModelModality
|
||||
from zhenxun.ui.builders import MarkdownBuilder, TableBuilder
|
||||
from zhenxun.ui.models import StatusBadgeCell, TextCell
|
||||
|
||||
|
||||
@@ -33,10 +33,10 @@ class Presenters:
|
||||
title = "LLM模型列表" + (" (所有已配置模型)" if show_all else " (仅可用)")
|
||||
|
||||
if not models:
|
||||
builder = TableBuilder(
|
||||
title=title, tip="当前没有配置任何LLM模型。"
|
||||
).set_headers(["提供商", "模型名称", "API类型", "状态"])
|
||||
return await renderer_service.render(builder.build())
|
||||
table = ui.table(title=title, tip="当前没有配置任何LLM模型。").set_headers(
|
||||
["提供商", "模型名称", "API类型", "状态"]
|
||||
)
|
||||
return await renderer_service.render(table)
|
||||
|
||||
column_name = ["提供商", "模型名称", "API类型", "状态"]
|
||||
rows_data = []
|
||||
@@ -55,13 +55,13 @@ class Presenters:
|
||||
]
|
||||
)
|
||||
|
||||
builder = TableBuilder(
|
||||
table = ui.table(
|
||||
title=title, tip="使用 `llm info <Provider/ModelName>` 查看详情"
|
||||
)
|
||||
builder.set_headers(column_name)
|
||||
builder.set_column_alignments(["left", "left", "left", "center"])
|
||||
builder.add_rows(rows_data)
|
||||
return await renderer_service.render(builder.build(), use_cache=True)
|
||||
table.set_headers(column_name)
|
||||
table.set_column_alignments(["left", "left", "left", "center"])
|
||||
table.add_rows(rows_data)
|
||||
return await renderer_service.render(table, use_cache=True)
|
||||
|
||||
@staticmethod
|
||||
async def format_model_details_as_markdown_image(details: dict[str, Any]) -> bytes:
|
||||
@@ -82,25 +82,25 @@ class Presenters:
|
||||
if caps.is_embedding_model:
|
||||
cap_list.append("文本嵌入")
|
||||
|
||||
builder = MarkdownBuilder()
|
||||
builder.head(f"🔎 模型详情: {provider.name}/{model.model_name}", 1)
|
||||
builder.text("---")
|
||||
builder.head("提供商信息", 2)
|
||||
builder.text(f"- **名称**: {provider.name}")
|
||||
builder.text(f"- **API 类型**: {provider.api_type}")
|
||||
builder.text(f"- **API Base**: {provider.api_base or '默认'}")
|
||||
md = ui.markdown("")
|
||||
md.head(f"🔎 模型详情: {provider.name}/{model.model_name}", 1)
|
||||
md.text("---")
|
||||
md.head("提供商信息", 2)
|
||||
md.text(f"- **名称**: {provider.name}")
|
||||
md.text(f"- **API 类型**: {provider.api_type}")
|
||||
md.text(f"- **API Base**: {provider.api_base or '默认'}")
|
||||
|
||||
builder.head("模型详情", 2)
|
||||
md.head("模型详情", 2)
|
||||
|
||||
temp_value = model.temperature or provider.temperature or "未设置"
|
||||
token_value = model.max_tokens or provider.max_tokens or "未设置"
|
||||
|
||||
builder.text(f"- **名称**: {model.model_name}")
|
||||
builder.text(f"- **默认温度**: {temp_value}")
|
||||
builder.text(f"- **最大Token**: {token_value}")
|
||||
builder.text(f"- **核心能力**: {', '.join(cap_list) or '纯文本'}")
|
||||
md.text(f"- **名称**: {model.model_name}")
|
||||
md.text(f"- **默认温度**: {temp_value}")
|
||||
md.text(f"- **最大Token**: {token_value}")
|
||||
md.text(f"- **核心能力**: {', '.join(cap_list) or '纯文本'}")
|
||||
|
||||
return await renderer_service.render(builder.with_style("light").build())
|
||||
return await renderer_service.render(md.with_style("light"))
|
||||
|
||||
@staticmethod
|
||||
async def format_key_status_as_image(
|
||||
@@ -167,10 +167,8 @@ class Presenters:
|
||||
]
|
||||
)
|
||||
|
||||
builder = TableBuilder(
|
||||
title=title, tip="使用 `llm reset-key <Provider>` 重置Key状态"
|
||||
)
|
||||
builder.set_headers(
|
||||
table = ui.table(title=title, tip="使用 `llm reset-key <Provider>` 重置Key状态")
|
||||
table.set_headers(
|
||||
[
|
||||
"Key (部分)",
|
||||
"状态",
|
||||
@@ -181,5 +179,5 @@ class Presenters:
|
||||
"建议操作",
|
||||
]
|
||||
)
|
||||
builder.add_rows(data_list)
|
||||
return await renderer_service.render(builder.build(), use_cache=False)
|
||||
table.add_rows(data_list)
|
||||
return await renderer_service.render(table, use_cache=False)
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import Any
|
||||
from zhenxun import ui
|
||||
from zhenxun.models.scheduled_job import ScheduledJob
|
||||
from zhenxun.services import scheduler_manager
|
||||
from zhenxun.ui.builders import TableBuilder
|
||||
from zhenxun.ui.models import StatusBadgeCell, TextCell
|
||||
from zhenxun.utils.pydantic_compat import model_json_schema
|
||||
|
||||
@@ -159,14 +158,14 @@ async def format_schedule_list_as_image(
|
||||
if not data_list:
|
||||
return "没有找到任何相关的定时任务。"
|
||||
|
||||
builder = TableBuilder(
|
||||
table = ui.table(
|
||||
title, f"第 {current_page}/{total_pages} 页,共 {total_items} 条任务"
|
||||
)
|
||||
builder.set_headers(
|
||||
table.set_headers(
|
||||
["ID", "插件", "Bot", "目标", "下次运行", "规则", "参数", "状态"]
|
||||
).add_rows(data_list)
|
||||
return await ui.render(
|
||||
builder.build(),
|
||||
table,
|
||||
viewport={"width": 1400, "height": 10},
|
||||
device_scale_factor=2,
|
||||
)
|
||||
|
||||
@@ -146,11 +146,10 @@ async def gold_rank(session: Uninfo, group_id: str | None, num: int) -> bytes |
|
||||
else:
|
||||
title = "金币全局排行"
|
||||
tip = f"你的排名在全局第 {index} 位哦!"
|
||||
from zhenxun.ui.builders import TableBuilder
|
||||
|
||||
builder = TableBuilder(title, tip)
|
||||
builder.set_headers(column_name).add_rows(data_list)
|
||||
return await ui.render(builder.build())
|
||||
table = ui.table(title, tip)
|
||||
table.set_headers(column_name).add_rows(data_list)
|
||||
return await ui.render(table)
|
||||
|
||||
|
||||
class ShopManage:
|
||||
@@ -551,11 +550,10 @@ class ShopManage:
|
||||
return None
|
||||
|
||||
column_name = ["-", "使用ID", "名称", "数量", "简介"]
|
||||
from zhenxun.ui.builders import TableBuilder
|
||||
|
||||
builder = TableBuilder(f"{name}的道具仓库", "通过 使用道具[ID/名称] 令道具生效")
|
||||
builder.set_headers(column_name).add_rows(table_rows)
|
||||
return await ui.render(builder.build())
|
||||
table = ui.table(f"{name}的道具仓库", "通过 使用道具[ID/名称] 令道具生效")
|
||||
table.set_headers(column_name).add_rows(table_rows)
|
||||
return await ui.render(table)
|
||||
|
||||
@classmethod
|
||||
async def my_cost(cls, user_id: str, platform: str | None = None) -> int:
|
||||
|
||||
@@ -105,11 +105,10 @@ class SignManage:
|
||||
else:
|
||||
title = "好感度全局排行"
|
||||
tip = f"你的排名在全局第 {index} 位哦!"
|
||||
from zhenxun.ui.builders import TableBuilder
|
||||
|
||||
builder = TableBuilder(title, tip)
|
||||
builder.set_headers(column_name).add_rows(data_list)
|
||||
return await ui.render(builder.build())
|
||||
table = ui.table(title, tip)
|
||||
table.set_headers(column_name).add_rows(data_list)
|
||||
return await ui.render(table)
|
||||
|
||||
@classmethod
|
||||
async def sign(
|
||||
|
||||
@@ -21,12 +21,12 @@ from nonebot_plugin_alconna import (
|
||||
from nonebot_plugin_session import EventSession
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from zhenxun import ui
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.services import group_settings_service, renderer_service
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.services.tags import tag_manager
|
||||
from zhenxun.ui import builders as ui
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
@@ -254,15 +254,15 @@ async def handle_list(arp: Arparma, bot: Bot, event: Event):
|
||||
|
||||
rows.append(row_data)
|
||||
|
||||
builder = ui.TableBuilder(
|
||||
table = ui.table(
|
||||
title=f"插件 '{plugin_name_str}' 全群配置",
|
||||
tip=f"共查询 {len(rows)} 个群组",
|
||||
)
|
||||
builder.set_headers(headers).add_rows(rows)
|
||||
table.set_headers(headers).add_rows(rows)
|
||||
|
||||
viewport_width = 300 + len(config_keys) * 280
|
||||
img = await renderer_service.render(
|
||||
builder.build(), viewport={"width": viewport_width, "height": 10}
|
||||
table, viewport={"width": viewport_width, "height": 10}
|
||||
)
|
||||
await MessageUtils.build_message(img).finish()
|
||||
|
||||
@@ -277,20 +277,20 @@ async def handle_list(arp: Arparma, bot: Bot, event: Event):
|
||||
f"插件 '{plugin_name_str}' 没有可配置的全局项。"
|
||||
).finish()
|
||||
|
||||
builder = ui.TableBuilder(
|
||||
table = ui.table(
|
||||
title=f"插件 '{plugin_name_str}' 全局可配置项",
|
||||
tip=(
|
||||
f"位于 config.yaml, 使用 pconf set <key>=<value> "
|
||||
f"-p {plugin_name_str} --global 进行设置"
|
||||
),
|
||||
)
|
||||
builder.set_headers(["配置项", "当前值", "类型", "描述"])
|
||||
table.set_headers(["配置项", "当前值", "类型", "描述"])
|
||||
|
||||
for key, config_model in config_group.configs.items():
|
||||
type_name = getattr(
|
||||
config_model.type, "__name__", str(config_model.type)
|
||||
)
|
||||
builder.add_row(
|
||||
table.add_row(
|
||||
[
|
||||
key,
|
||||
truncate_text(str(config_model.value), 20),
|
||||
@@ -299,7 +299,7 @@ async def handle_list(arp: Arparma, bot: Bot, event: Event):
|
||||
]
|
||||
)
|
||||
|
||||
img = await renderer_service.render(builder.build())
|
||||
img = await renderer_service.render(table)
|
||||
await MessageUtils.build_message(img).finish()
|
||||
else:
|
||||
model = await get_plugin_config_model(plugin_name_str)
|
||||
@@ -309,11 +309,11 @@ async def handle_list(arp: Arparma, bot: Bot, event: Event):
|
||||
f"插件 '{plugin_name_str}' 不支持分群配置。"
|
||||
).finish()
|
||||
|
||||
builder = ui.TableBuilder(
|
||||
table = ui.table(
|
||||
title=f"插件 '{plugin_name_str}' 可配置项",
|
||||
tip=f"使用 pconf set <key>=<value> -p {plugin_name_str} 进行设置",
|
||||
)
|
||||
builder.set_headers(["配置项", "类型", "描述", "默认值"])
|
||||
table.set_headers(["配置项", "类型", "描述", "默认值"])
|
||||
|
||||
for field in model_fields_list:
|
||||
type_name = getattr(field.annotation, "__name__", str(field.annotation))
|
||||
@@ -323,9 +323,9 @@ async def handle_list(arp: Arparma, bot: Bot, event: Event):
|
||||
if field.field_info.default is not None
|
||||
else "无"
|
||||
)
|
||||
builder.add_row([field.name, type_name, description, default_value])
|
||||
table.add_row([field.name, type_name, description, default_value])
|
||||
|
||||
img = await renderer_service.render(builder.build())
|
||||
img = await renderer_service.render(table)
|
||||
await MessageUtils.build_message(img).finish()
|
||||
|
||||
else:
|
||||
|
||||
@@ -56,6 +56,15 @@ __plugin_meta__ = PluginMetadata(
|
||||
default_value=False,
|
||||
type=bool,
|
||||
),
|
||||
RegisterConfig(
|
||||
module="UI",
|
||||
key="HOT_RELOAD",
|
||||
value=False,
|
||||
help="是否开启UI热重载模式 (修改HTML/CSS后立即生效,"
|
||||
"性能较低,仅建议开发时开启)",
|
||||
default_value=False,
|
||||
type=bool,
|
||||
),
|
||||
],
|
||||
).to_dict(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user