Compare commits

...

3 Commits

Author SHA1 Message Date
Rumio
6861dfeeed
Merge e3d49c7105 into c7ef6fdb17 2025-09-11 10:37:11 +08:00
Rumio
c7ef6fdb17
feat(ui): 增强表格构建器并完善组件模型文档 (#2048)
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
*  feat(table): 添加 ComponentCell 以支持表格单元格中嵌入可渲染组件

*  feat(ui): 增强表格构建器并完善组件模型文档

- 增强 `TableBuilder`,新增 `_normalize_cell` 辅助方法,支持自动将原生数据类型(如 `str`, `int`, `Path`)转换为 `TableCell` 模型,简化了表格行的创建。
- 完善 `zhenxun/ui/models` 目录下所有组件模型字段的 `description` 属性和文档字符串,显著提升了代码可读性和开发者体验。
- 优化 `shop/_data_source.py` 中 `gold_rank` 函数的平台路径判断格式,并统一 `my_props` 函数中图标路径的处理逻辑。

* 🚨 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-09-11 10:31:49 +08:00
webjoin111
e3d49c7105 feat(auto_update): 增强自动更新与版本检查
- 优化 `检查更新` 默认行为,未指定类型时直接显示版本信息
- 扩展版本详情显示:当前版本、最新开发版/正式版(含日期)、资源版本及更新提示
- 新增更新后资源兼容性检查,自动读取 `resources.spec` 并提示更新
- 使用 `asyncio.gather` 并发获取版本信息,引入 `packaging` 库提高比较准确性
- 优化错误处理与日志记录
2025-09-01 15:10:25 +08:00
28 changed files with 422 additions and 75 deletions

View File

@ -84,13 +84,16 @@ async def _(
): ):
result = "" result = ""
await MessageUtils.build_message("正在进行检查更新...").send(reply_to=True) await MessageUtils.build_message("正在进行检查更新...").send(reply_to=True)
if not ver_type.available:
result += await UpdateManager.check_version()
logger.info("查看当前版本...", "检查更新", session=session)
await MessageUtils.build_message(result).finish()
return
ver_type_str = ver_type.result ver_type_str = ver_type.result
source_str = source.result source_str = source.result
if ver_type_str in {"main", "release"}: if ver_type_str in {"main", "release"}:
if not ver_type.available:
result += await UpdateManager.check_version()
logger.info("查看当前版本...", "检查更新", session=session)
await MessageUtils.build_message(result).finish()
try: try:
result += await UpdateManager.update_zhenxun( result += await UpdateManager.update_zhenxun(
bot, bot,

View File

@ -1,37 +1,135 @@
import asyncio
from typing import Literal from typing import Literal
from nonebot.adapters import Bot from nonebot.adapters import Bot
from packaging.specifiers import SpecifierSet
from packaging.version import InvalidVersion, Version
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.http_utils import AsyncHttpx
from zhenxun.utils.manager.virtual_env_package_manager import VirtualEnvPackageManager from zhenxun.utils.manager.virtual_env_package_manager import VirtualEnvPackageManager
from zhenxun.utils.manager.zhenxun_repo_manager import ( from zhenxun.utils.manager.zhenxun_repo_manager import (
ZhenxunRepoConfig, ZhenxunRepoConfig,
ZhenxunRepoManager, ZhenxunRepoManager,
) )
from zhenxun.utils.platform import PlatformUtils from zhenxun.utils.platform import PlatformUtils
from zhenxun.utils.repo_utils import RepoFileManager
LOG_COMMAND = "AutoUpdate" LOG_COMMAND = "AutoUpdate"
class UpdateManager: class UpdateManager:
@staticmethod
async def _get_latest_commit_date(owner: str, repo: str, path: str) -> str:
"""获取文件最新 commit 日期"""
api_url = f"https://api.github.com/repos/{owner}/{repo}/commits"
params = {"path": path, "page": 1, "per_page": 1}
try:
data = await AsyncHttpx.get_json(api_url, params=params)
if data and isinstance(data, list) and data[0]:
date_str = data[0]["commit"]["committer"]["date"]
return date_str.split("T")[0]
except Exception as e:
logger.warning(f"获取 {owner}/{repo}/{path} 的 commit 日期失败", e=e)
return "获取失败"
@classmethod @classmethod
async def check_version(cls) -> str: async def check_version(cls) -> str:
"""检查更新版本 """检查真寻和资源的版本"""
bot_cur_version = cls.__get_version()
返回: release_task = ZhenxunRepoManager.zhenxun_get_latest_releases_data()
str: 更新信息 dev_version_task = RepoFileManager.get_file_content(
""" ZhenxunRepoConfig.ZHENXUN_BOT_GITHUB_URL, "__version__"
cur_version = cls.__get_version()
release_data = await ZhenxunRepoManager.zhenxun_get_latest_releases_data()
if not release_data:
return "检查更新获取版本失败..."
return (
"检测到当前版本更新\n"
f"当前版本:{cur_version}\n"
f"最新版本:{release_data.get('name')}\n"
f"创建日期:{release_data.get('created_at')}\n"
f"更新内容:\n{release_data.get('body')}"
) )
bot_commit_date_task = cls._get_latest_commit_date(
"HibiKier", "zhenxun_bot", "__version__"
)
res_commit_date_task = cls._get_latest_commit_date(
"zhenxun-org", "zhenxun-bot-resources", "__version__"
)
(
release_data,
dev_version_text,
bot_commit_date,
res_commit_date,
) = await asyncio.gather(
release_task,
dev_version_task,
bot_commit_date_task,
res_commit_date_task,
return_exceptions=True,
)
if isinstance(release_data, dict):
bot_release_version = release_data.get("name", "获取失败")
bot_release_date = release_data.get("created_at", "").split("T")[0]
else:
bot_release_version = "获取失败"
bot_release_date = "获取失败"
logger.warning(f"获取 Bot release 信息失败: {release_data}")
if isinstance(dev_version_text, str):
bot_dev_version = dev_version_text.split(":")[-1].strip()
else:
bot_dev_version = "获取失败"
bot_commit_date = "获取失败"
logger.warning(f"获取 Bot dev 版本信息失败: {dev_version_text}")
bot_update_hint = ""
try:
cur_base_v = bot_cur_version.split("-")[0].lstrip("v")
dev_base_v = bot_dev_version.split("-")[0].lstrip("v")
if Version(cur_base_v) < Version(dev_base_v):
bot_update_hint = "\n-> 发现新开发版本, 可用 `检查更新 main` 更新"
elif (
Version(cur_base_v) == Version(dev_base_v)
and bot_cur_version != bot_dev_version
):
bot_update_hint = "\n-> 发现新开发版本, 可用 `检查更新 main` 更新"
except (InvalidVersion, TypeError, IndexError):
if bot_cur_version != bot_dev_version and bot_dev_version != "获取失败":
bot_update_hint = "\n-> 发现新开发版本, 可用 `检查更新 main` 更新"
bot_update_info = (
f"当前版本: {bot_cur_version}\n"
f"最新开发版: {bot_dev_version} (更新于: {bot_commit_date})\n"
f"最新正式版: {bot_release_version} (发布于: {bot_release_date})"
f"{bot_update_hint}"
)
res_version_file = ZhenxunRepoConfig.RESOURCE_PATH / "__version__"
res_cur_version = "未找到"
if res_version_file.exists():
if text := res_version_file.open(encoding="utf8").readline():
res_cur_version = text.split(":")[-1].strip()
res_latest_version = "获取失败"
try:
res_latest_version_text = await RepoFileManager.get_file_content(
ZhenxunRepoConfig.RESOURCE_GITHUB_URL, "__version__"
)
res_latest_version = res_latest_version_text.split(":")[-1].strip()
except Exception as e:
res_commit_date = "获取失败"
logger.warning(f"获取资源版本信息失败: {e}")
res_update_hint = ""
try:
if Version(res_cur_version) < Version(res_latest_version):
res_update_hint = "\n-> 发现新资源版本, 可用 `检查更新 resource` 更新"
except (InvalidVersion, TypeError):
pass
res_update_info = (
f"当前版本: {res_cur_version}\n"
f"最新版本: {res_latest_version} (更新于: {res_commit_date})"
f"{res_update_hint}"
)
return f"『绪山真寻 Bot』\n{bot_update_info}\n\n『真寻资源』\n{res_update_info}"
@classmethod @classmethod
async def update_webui( async def update_webui(
@ -125,6 +223,7 @@ class UpdateManager:
f"检测真寻已更新,当前版本:{cur_version}\n开始更新...", f"检测真寻已更新,当前版本:{cur_version}\n开始更新...",
user_id, user_id,
) )
result_message = ""
if zip: if zip:
new_version = await ZhenxunRepoManager.zhenxun_zip_update(version_type) new_version = await ZhenxunRepoManager.zhenxun_zip_update(version_type)
await PlatformUtils.send_superuser( await PlatformUtils.send_superuser(
@ -133,7 +232,7 @@ class UpdateManager:
await VirtualEnvPackageManager.install_requirement( await VirtualEnvPackageManager.install_requirement(
ZhenxunRepoConfig.REQUIREMENTS_FILE ZhenxunRepoConfig.REQUIREMENTS_FILE
) )
return ( result_message = (
f"版本更新完成!\n版本: {cur_version} -> {new_version}\n" f"版本更新完成!\n版本: {cur_version} -> {new_version}\n"
"请重新启动真寻以完成更新!" "请重新启动真寻以完成更新!"
) )
@ -155,13 +254,54 @@ class UpdateManager:
await VirtualEnvPackageManager.install_requirement( await VirtualEnvPackageManager.install_requirement(
ZhenxunRepoConfig.REQUIREMENTS_FILE ZhenxunRepoConfig.REQUIREMENTS_FILE
) )
return ( result_message = (
f"版本更新完成!\n" f"版本更新完成!\n"
f"版本: {cur_version} -> {result.new_version}\n" f"版本: {cur_version} -> {result.new_version}\n"
f"变更文件个数: {len(result.changed_files)}" f"变更文件个数: {len(result.changed_files)}"
f"{'' if source == 'git' else '(阿里云更新不支持查看变更文件)'}\n" f"{'' if source == 'git' else '(阿里云更新不支持查看变更文件)'}\n"
"请重新启动真寻以完成更新!" "请重新启动真寻以完成更新!"
) )
resource_warning = ""
if version_type == "main":
try:
spec_content = await RepoFileManager.get_file_content(
ZhenxunRepoConfig.ZHENXUN_BOT_GITHUB_URL, "resources.spec"
)
required_spec_str = None
for line in spec_content.splitlines():
if line.startswith("require_resources_version:"):
required_spec_str = line.split(":", 1)[1].strip().strip("\"'")
break
if required_spec_str:
res_version_file = ZhenxunRepoConfig.RESOURCE_PATH / "__version__"
local_res_version_str = "0.0.0"
if res_version_file.exists():
if text := res_version_file.open(encoding="utf8").readline():
local_res_version_str = text.split(":")[-1].strip()
spec = SpecifierSet(required_spec_str)
local_ver = Version(local_res_version_str)
if not spec.contains(local_ver):
warning_header = (
f"⚠️ **资源版本不兼容!**\n"
f"当前代码需要资源版本: `{required_spec_str}`\n"
f"您当前的资源版本是: `{local_res_version_str}`\n"
"**将自动为您更新资源文件...**"
)
await PlatformUtils.send_superuser(bot, warning_header, user_id)
resource_update_source = None if zip else source
resource_update_result = await cls.update_resources(
source=resource_update_source, force=force
)
resource_warning = (
f"\n\n{warning_header}\n{resource_update_result}"
)
except Exception as e:
logger.warning(f"检查资源版本兼容性时出错: {e}", LOG_COMMAND, e=e)
resource_warning = (
"\n\n⚠️ 检查资源版本兼容性时出错,建议手动运行 `检查更新 resource`"
)
return result_message + resource_warning
@classmethod @classmethod
def __get_version(cls) -> str: def __get_version(cls) -> str:

View File

@ -132,7 +132,7 @@ async def gold_rank(session: Uninfo, group_id: str | None, num: int) -> bytes |
else TextCell(content=""), else TextCell(content=""),
TextCell(content=uid2name.get(user[0]) or user[0]), TextCell(content=uid2name.get(user[0]) or user[0]),
TextCell(content=str(user[1]), bold=True), TextCell(content=str(user[1]), bold=True),
ImageCell(src=platform_path) ImageCell(src=platform_path.resolve().as_uri())
if (platform_path := PLATFORM_PATH.get(platform)) if (platform_path := PLATFORM_PATH.get(platform))
else TextCell(content=""), else TextCell(content=""),
] ]
@ -529,18 +529,18 @@ class ShopManage:
if not prop: if not prop:
continue continue
icon = "" icon = None
if prop.icon: if prop.icon:
icon_path = ICON_PATH / prop.icon icon_path = ICON_PATH / prop.icon
icon = icon_path if icon_path.exists() else "" icon = icon_path if icon_path.exists() else None
table_rows.append( table_rows.append(
[ [
ImageCell(src=icon, height=33, width=33), icon,
TextCell(content=i), i,
TextCell(content=prop.goods_name), prop.goods_name,
TextCell(content=user.props[prop_uuid]), user.props[prop_uuid],
TextCell(content=prop.goods_description), prop.goods_description,
] ]
) )

View File

@ -91,7 +91,7 @@ class SignManage:
TextCell(content=uid2name.get(user[0]) or user[0]), TextCell(content=uid2name.get(user[0]) or user[0]),
TextCell(content=str(user[1]), bold=True), TextCell(content=str(user[1]), bold=True),
TextCell(content=str(user[2])), TextCell(content=str(user[2])),
ImageCell(src=platform_path) ImageCell(src=platform_path.resolve().as_uri())
if (platform_path := PLATFORM_PATH.get(platform)) if (platform_path := PLATFORM_PATH.get(platform))
else TextCell(content=""), else TextCell(content=""),
] ]

View File

@ -1,6 +1,13 @@
from typing import Literal from pathlib import Path
from typing import Any, Literal
from ...models.core.table import TableCell, TableData from ...models.core.table import (
BaseCell,
ImageCell,
TableCell,
TableData,
TextCell,
)
from ..base import BaseBuilder from ..base import BaseBuilder
__all__ = ["TableBuilder"] __all__ = ["TableBuilder"]
@ -13,6 +20,28 @@ class TableBuilder(BaseBuilder[TableData]):
data_model = TableData(title=title, tip=tip, headers=[], rows=[]) data_model = TableData(title=title, tip=tip, headers=[], rows=[])
super().__init__(data_model, template_name="components/core/table") super().__init__(data_model, template_name="components/core/table")
def _normalize_cell(self, cell_data: Any) -> TableCell:
"""内部辅助方法将各种原生数据类型转换为TableCell模型。"""
if isinstance(cell_data, BaseCell):
return cell_data # type: ignore
if isinstance(cell_data, str | int | float):
return TextCell(content=str(cell_data))
if isinstance(cell_data, Path):
return ImageCell(src=cell_data.resolve().as_uri())
if isinstance(cell_data, tuple) and len(cell_data) == 3:
if (
isinstance(cell_data[0], Path)
and isinstance(cell_data[1], int)
and isinstance(cell_data[2], int)
):
return ImageCell(
src=cell_data[0].resolve().as_uri(),
width=cell_data[1],
height=cell_data[2],
)
return TextCell(content="")
def set_headers(self, headers: list[str]) -> "TableBuilder": def set_headers(self, headers: list[str]) -> "TableBuilder":
""" """
设置表格的表头 设置表格的表头
@ -57,12 +86,13 @@ class TableBuilder(BaseBuilder[TableData]):
返回: 返回:
TableBuilder: 当前构建器实例以支持链式调用 TableBuilder: 当前构建器实例以支持链式调用
""" """
self._data.rows.append(row) normalized_row = [self._normalize_cell(cell) for cell in row]
self._data.rows.append(normalized_row)
return self return self
def add_rows(self, rows: list[list[TableCell]]) -> "TableBuilder": def add_rows(self, rows: list[list[TableCell]]) -> "TableBuilder":
""" """
向表格中批量添加多行数据 向表格中批量添加多行数据, 并自动转换原生类型
参数: 参数:
rows: 一个包含多行数据的列表 rows: 一个包含多行数据的列表
@ -70,5 +100,6 @@ class TableBuilder(BaseBuilder[TableData]):
返回: 返回:
TableBuilder: 当前构建器实例以支持链式调用 TableBuilder: 当前构建器实例以支持链式调用
""" """
self._data.rows.extend(rows) for row in rows:
self.add_row(row)
return self return self

View File

@ -12,6 +12,7 @@ from .components import (
from .core import ( from .core import (
BaseCell, BaseCell,
CodeElement, CodeElement,
ComponentCell,
HeadingElement, HeadingElement,
ImageCell, ImageCell,
ImageElement, ImageElement,
@ -49,6 +50,7 @@ __all__ = [
"BaseCell", "BaseCell",
"BaseChartData", "BaseChartData",
"CodeElement", "CodeElement",
"ComponentCell",
"Divider", "Divider",
"EChartsData", "EChartsData",
"HeadingElement", "HeadingElement",

View File

@ -11,44 +11,68 @@ from .core.base import RenderableComponent
class EChartsTitle(BaseModel): class EChartsTitle(BaseModel):
text: str text: str
"""图表主标题"""
left: Literal["left", "center", "right"] = "center" left: Literal["left", "center", "right"] = "center"
"""标题水平对齐方式"""
class EChartsAxis(BaseModel): class EChartsAxis(BaseModel):
type: Literal["category", "value", "time", "log"] type: Literal["category", "value", "time", "log"]
"""坐标轴类型"""
data: list[Any] | None = None data: list[Any] | None = None
"""类目数据"""
show: bool = True show: bool = True
"""是否显示坐标轴"""
class EChartsSeries(BaseModel): class EChartsSeries(BaseModel):
type: str type: str
"""系列类型 (e.g., 'bar', 'line', 'pie')"""
data: list[Any] data: list[Any]
"""系列数据"""
name: str | None = None name: str | None = None
"""系列名称,用于 tooltip 的显示"""
label: dict[str, Any] | None = None label: dict[str, Any] | None = None
"""图形上的文本标签"""
itemStyle: dict[str, Any] | None = None itemStyle: dict[str, Any] | None = None
"""图形样式"""
barMaxWidth: int | None = None barMaxWidth: int | None = None
"""柱条的最大宽度"""
smooth: bool | None = None smooth: bool | None = None
"""是否平滑显示折线"""
class EChartsTooltip(BaseModel): class EChartsTooltip(BaseModel):
trigger: Literal["item", "axis", "none"] = "item" trigger: Literal["item", "axis", "none"] = Field("item", description="触发类型")
"""触发类型"""
class EChartsGrid(BaseModel): class EChartsGrid(BaseModel):
left: str | None = None left: str | None = None
"""grid 组件离容器左侧的距离"""
right: str | None = None right: str | None = None
"""grid 组件离容器右侧的距离"""
top: str | None = None top: str | None = None
"""grid 组件离容器上侧的距离"""
bottom: str | None = None bottom: str | None = None
"""grid 组件离容器下侧的距离"""
containLabel: bool = True containLabel: bool = True
"""grid 区域是否包含坐标轴的刻度标签"""
class BaseChartData(RenderableComponent, ABC): class BaseChartData(RenderableComponent, ABC):
"""所有图表数据模型的基类""" """所有图表数据模型的基类"""
style_name: str | None = None style_name: str | None = None
chart_id: str = Field(default_factory=lambda: f"chart-{uuid.uuid4().hex}") """组件的样式名称"""
chart_id: str = Field(
default_factory=lambda: f"chart-{uuid.uuid4().hex}",
description="图表的唯一ID用于前端渲染",
)
"""图表的唯一ID用于前端渲染"""
echarts_options: dict[str, Any] | None = None echarts_options: dict[str, Any] | None = None
"""原始ECharts选项用于高级自定义"""
@abstractmethod @abstractmethod
def build_option(self) -> dict[str, Any]: def build_option(self) -> dict[str, Any]:
@ -70,21 +94,37 @@ class BaseChartData(RenderableComponent, ABC):
class EChartsData(BaseChartData): class EChartsData(BaseChartData):
"""统一的 ECharts 图表数据模型""" """统一的 ECharts 图表数据模型"""
template_path: str = Field(..., exclude=True) template_path: str = Field(..., exclude=True, description="图表组件的模板路径")
title_model: EChartsTitle | None = Field(None, alias="title") """图表组件的模板路径"""
grid_model: EChartsGrid | None = Field(None, alias="grid") title_model: EChartsTitle | None = Field(
tooltip_model: EChartsTooltip | None = Field(None, alias="tooltip") None, alias="title", description="标题组件"
x_axis_model: EChartsAxis | None = Field(None, alias="xAxis") )
y_axis_model: EChartsAxis | None = Field(None, alias="yAxis") """标题组件"""
series_models: list[EChartsSeries] = Field(default_factory=list, alias="series") grid_model: EChartsGrid | None = Field(None, alias="grid", description="网格组件")
legend_model: dict[str, Any] | None = Field(default_factory=dict, alias="legend") """网格组件"""
tooltip_model: EChartsTooltip | None = Field(
None, alias="tooltip", description="提示框组件"
)
"""提示框组件"""
x_axis_model: EChartsAxis | None = Field(None, alias="xAxis", description="X轴配置")
"""X轴配置"""
y_axis_model: EChartsAxis | None = Field(None, alias="yAxis", description="Y轴配置")
"""Y轴配置"""
series_models: list[EChartsSeries] = Field(
default_factory=list, alias="series", description="系列列表"
)
"""系列列表"""
legend_model: dict[str, Any] | None = Field(
default_factory=dict, alias="legend", description="图例组件"
)
"""图例组件"""
raw_options: dict[str, Any] = Field( raw_options: dict[str, Any] = Field(
default_factory=dict, description="用于 set_option 的原始覆盖选项" default_factory=dict, description="用于 set_option 的原始覆盖选项"
) )
"""用于 set_option 的原始覆盖选项"""
background_image: str | None = Field( background_image: str | None = Field(None, description="用于横向柱状图的背景图片")
None, description="【兼容】用于横向柱状图的背景图片" """用于横向柱状图的背景图片"""
)
def build_option(self) -> dict[str, Any]: def build_option(self) -> dict[str, Any]:
"""将 Pydantic 模型序列化为 ECharts 的 option 字典。""" """将 Pydantic 模型序列化为 ECharts 的 option 字典。"""

View File

@ -14,9 +14,13 @@ class Alert(RenderableComponent):
type: Literal["info", "success", "warning", "error"] = Field( type: Literal["info", "success", "warning", "error"] = Field(
default="info", description="提示框的类型,决定了颜色和图标" default="info", description="提示框的类型,决定了颜色和图标"
) )
"""提示框的类型,决定了颜色和图标"""
title: str = Field(..., description="提示框的标题") title: str = Field(..., description="提示框的标题")
"""提示框的标题"""
content: str = Field(..., description="提示框的主要内容") content: str = Field(..., description="提示框的主要内容")
"""提示框的主要内容"""
show_icon: bool = Field(default=True, description="是否显示与类型匹配的图标") show_icon: bool = Field(default=True, description="是否显示与类型匹配的图标")
"""是否显示与类型匹配的图标"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -12,8 +12,11 @@ class Avatar(RenderableComponent):
component_type: Literal["avatar"] = "avatar" component_type: Literal["avatar"] = "avatar"
src: str = Field(..., description="头像的URL或Base64数据URI") src: str = Field(..., description="头像的URL或Base64数据URI")
"""头像的URL或Base64数据URI"""
shape: Literal["circle", "square"] = Field("circle", description="头像形状") shape: Literal["circle", "square"] = Field("circle", description="头像形状")
"""头像形状"""
size: int = Field(50, description="头像尺寸(像素)") size: int = Field(50, description="头像尺寸(像素)")
"""头像尺寸(像素)"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:
@ -25,10 +28,13 @@ class AvatarGroup(RenderableComponent):
component_type: Literal["avatar_group"] = "avatar_group" component_type: Literal["avatar_group"] = "avatar_group"
avatars: list[Avatar] = Field(default_factory=list, description="头像列表") avatars: list[Avatar] = Field(default_factory=list, description="头像列表")
"""头像列表"""
spacing: int = Field(-15, description="头像间的间距(负数表示重叠)") spacing: int = Field(-15, description="头像间的间距(负数表示重叠)")
"""头像间的间距(负数表示重叠)"""
max_count: int | None = Field( max_count: int | None = Field(
None, description="最多显示的头像数量,超出部分会显示为'+N'" None, description="最多显示的头像数量,超出部分会显示为'+N'"
) )
"""最多显示的头像数量,超出部分会显示为'+N'"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -12,10 +12,12 @@ class Badge(RenderableComponent):
component_type: Literal["badge"] = "badge" component_type: Literal["badge"] = "badge"
text: str = Field(..., description="徽章上显示的文本") text: str = Field(..., description="徽章上显示的文本")
"""徽章上显示的文本"""
color_scheme: Literal["primary", "success", "warning", "error", "info"] = Field( color_scheme: Literal["primary", "success", "warning", "error", "info"] = Field(
default="info", default="info",
description="预设的颜色方案", description="预设的颜色方案",
) )
"""预设的颜色方案"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -12,9 +12,13 @@ class Divider(RenderableComponent):
component_type: Literal["divider"] = "divider" component_type: Literal["divider"] = "divider"
margin: str = Field("2em 0", description="CSS margin属性控制分割线上下的间距") margin: str = Field("2em 0", description="CSS margin属性控制分割线上下的间距")
"""CSS margin属性控制分割线上下的间距"""
color: str = Field("#f7889c", description="分割线颜色") color: str = Field("#f7889c", description="分割线颜色")
"""分割线颜色"""
style: Literal["solid", "dashed", "dotted"] = Field("solid", description="线条样式") style: Literal["solid", "dashed", "dotted"] = Field("solid", description="线条样式")
"""线条样式"""
thickness: str = Field("1px", description="线条粗细") thickness: str = Field("1px", description="线条粗细")
"""线条粗细"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:
@ -26,9 +30,13 @@ class Rectangle(RenderableComponent):
component_type: Literal["rectangle"] = "rectangle" component_type: Literal["rectangle"] = "rectangle"
height: str = Field("50px", description="矩形的高度 (CSS value)") height: str = Field("50px", description="矩形的高度 (CSS value)")
"""矩形的高度 (CSS value)"""
background_color: str = Field("#fdf1f5", description="背景颜色") background_color: str = Field("#fdf1f5", description="背景颜色")
"""背景颜色"""
border: str = Field("1px solid #fce4ec", description="CSS border属性") border: str = Field("1px solid #fce4ec", description="CSS border属性")
"""CSS border属性"""
border_radius: str = Field("8px", description="CSS border-radius属性") border_radius: str = Field("8px", description="CSS border-radius属性")
"""CSS border-radius属性"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -12,17 +12,23 @@ class KpiCard(RenderableComponent):
component_type: Literal["kpi_card"] = "kpi_card" component_type: Literal["kpi_card"] = "kpi_card"
label: str = Field(..., description="指标的标签或名称") label: str = Field(..., description="指标的标签或名称")
"""指标的标签或名称"""
value: Any = Field(..., description="指标的主要数值") value: Any = Field(..., description="指标的主要数值")
"""指标的主要数值"""
unit: str | None = Field(default=None, description="数值的单位,可选") unit: str | None = Field(default=None, description="数值的单位,可选")
"""数值的单位,可选"""
change: str | None = Field( change: str | None = Field(
default=None, description="与上一周期的变化,例如 '+15%''-100'" default=None, description="与上一周期的变化,例如 '+15%''-100'"
) )
"""与上一周期的变化,例如 '+15%''-100'"""
change_type: Literal["positive", "negative", "neutral"] = Field( change_type: Literal["positive", "negative", "neutral"] = Field(
default="neutral", description="变化的类型,用于决定颜色" default="neutral", description="变化的类型,用于决定颜色"
) )
"""变化的类型,用于决定颜色"""
icon_svg: str | None = Field( icon_svg: str | None = Field(
default=None, description="卡片中显示的可选图标 (SVG path data)" default=None, description="卡片中显示的可选图标 (SVG path data)"
) )
"""卡片中显示的可选图标 (SVG path data)"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -12,12 +12,16 @@ class ProgressBar(RenderableComponent):
component_type: Literal["progress_bar"] = "progress_bar" component_type: Literal["progress_bar"] = "progress_bar"
progress: float = Field(..., ge=0, le=100, description="进度百分比 (0-100)") progress: float = Field(..., ge=0, le=100, description="进度百分比 (0-100)")
"""进度百分比 (0-100)"""
label: str | None = Field(default=None, description="显示在进度条上的可选文本") label: str | None = Field(default=None, description="显示在进度条上的可选文本")
"""显示在进度条上的可选文本"""
color_scheme: Literal["primary", "success", "warning", "error", "info"] = Field( color_scheme: Literal["primary", "success", "warning", "error", "info"] = Field(
default="primary", default="primary",
description="预设的颜色方案", description="预设的颜色方案",
) )
"""预设的颜色方案"""
animated: bool = Field(default=False, description="是否显示动画效果") animated: bool = Field(default=False, description="是否显示动画效果")
"""是否显示动画效果"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -11,10 +11,15 @@ class TimelineItem(BaseModel):
"""时间轴中的单个事件点。""" """时间轴中的单个事件点。"""
timestamp: str = Field(..., description="显示在时间点旁边的时间或标签") timestamp: str = Field(..., description="显示在时间点旁边的时间或标签")
"""显示在时间点旁边的时间或标签"""
title: str = Field(..., description="事件的标题") title: str = Field(..., description="事件的标题")
"""事件的标题"""
content: str = Field(..., description="事件的详细描述") content: str = Field(..., description="事件的详细描述")
"""事件的详细描述"""
icon: str | None = Field(default=None, description="可选的自定义图标SVG路径") icon: str | None = Field(default=None, description="可选的自定义图标SVG路径")
"""可选的自定义图标SVG路径"""
color: str | None = Field(default=None, description="可选的自定义颜色,覆盖默认") color: str | None = Field(default=None, description="可选的自定义颜色,覆盖默认")
"""可选的自定义颜色,覆盖默认"""
class Timeline(RenderableComponent): class Timeline(RenderableComponent):
@ -24,6 +29,7 @@ class Timeline(RenderableComponent):
items: list[TimelineItem] = Field( items: list[TimelineItem] = Field(
default_factory=list, description="时间轴项目列表" default_factory=list, description="时间轴项目列表"
) )
"""时间轴项目列表"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -12,11 +12,15 @@ class UserInfoBlock(RenderableComponent):
component_type: Literal["user_info_block"] = "user_info_block" component_type: Literal["user_info_block"] = "user_info_block"
avatar_url: str = Field(..., description="用户头像的URL") avatar_url: str = Field(..., description="用户头像的URL")
"""用户头像的URL"""
name: str = Field(..., description="用户的名称") name: str = Field(..., description="用户的名称")
"""用户的名称"""
subtitle: str | None = Field( subtitle: str | None = Field(
default=None, description="显示在名称下方的副标题 (如UID或角色)" default=None, description="显示在名称下方的副标题 (如UID或角色)"
) )
"""显示在名称下方的副标题 (如UID或角色)"""
tags: list[str] = Field(default_factory=list, description="附加的标签列表") tags: list[str] = Field(default_factory=list, description="附加的标签列表")
"""附加的标签列表"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -24,6 +24,7 @@ from .markdown import (
from .notebook import NotebookData, NotebookElement from .notebook import NotebookData, NotebookElement
from .table import ( from .table import (
BaseCell, BaseCell,
ComponentCell,
ImageCell, ImageCell,
RichTextCell, RichTextCell,
StatusBadgeCell, StatusBadgeCell,
@ -38,6 +39,7 @@ __all__ = [
"BaseCell", "BaseCell",
"CardData", "CardData",
"CodeElement", "CodeElement",
"ComponentCell",
"DetailsData", "DetailsData",
"DetailsItem", "DetailsItem",
"HeadingElement", "HeadingElement",

View File

@ -20,10 +20,15 @@ class RenderableComponent(BaseModel, Renderable):
""" """
_is_standalone_template: bool = False _is_standalone_template: bool = False
"""标记此组件是否为独立模板"""
inline_style: dict[str, str] | None = None inline_style: dict[str, str] | None = None
"""应用于组件根元素的内联CSS样式"""
component_css: str | None = None component_css: str | None = None
"""注入到页面的额外CSS字符串"""
extra_classes: list[str] | None = None extra_classes: list[str] | None = None
"""应用于组件根元素的额外CSS类名列表"""
variant: str | None = None variant: str | None = None
"""组件的变体/皮肤名称"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -7,8 +7,11 @@ class CardData(ContainerComponent):
"""通用卡片的数据模型,可以包含头部、内容和尾部""" """通用卡片的数据模型,可以包含头部、内容和尾部"""
header: RenderableComponent | None = None header: RenderableComponent | None = None
"""卡片的头部内容组件"""
content: RenderableComponent content: RenderableComponent
"""卡片的主要内容组件"""
footer: RenderableComponent | None = None footer: RenderableComponent | None = None
"""卡片的尾部内容组件"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -9,14 +9,18 @@ class DetailsItem(BaseModel):
"""描述列表中的单个项目""" """描述列表中的单个项目"""
label: str = Field(..., description="项目的标签/键") label: str = Field(..., description="项目的标签/键")
"""项目的标签/键"""
value: Any = Field(..., description="项目的值") value: Any = Field(..., description="项目的值")
"""项目的值"""
class DetailsData(RenderableComponent): class DetailsData(RenderableComponent):
"""描述列表(键值对)的数据模型""" """描述列表(键值对)的数据模型"""
title: str | None = Field(None, description="列表的可选标题") title: str | None = Field(None, description="列表的可选标题")
"""列表的可选标题"""
items: list[DetailsItem] = Field(default_factory=list, description="键值对项目列表") items: list[DetailsItem] = Field(default_factory=list, description="键值对项目列表")
"""键值对项目列表"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -12,20 +12,26 @@ class LayoutItem(BaseModel):
"""布局中的单个项目,现在持有可渲染组件的数据模型""" """布局中的单个项目,现在持有可渲染组件的数据模型"""
component: RenderableComponent = Field(..., description="要渲染的组件的数据模型") component: RenderableComponent = Field(..., description="要渲染的组件的数据模型")
"""要渲染的组件的数据模型"""
metadata: dict[str, Any] | None = Field(None, description="传递给模板的额外元数据") metadata: dict[str, Any] | None = Field(None, description="传递给模板的额外元数据")
"""传递给模板的额外元数据"""
class LayoutData(ContainerComponent): class LayoutData(ContainerComponent):
"""布局构建器的数据模型""" """布局构建器的数据模型"""
style_name: str | None = None style_name: str | None = None
"""应用于布局容器的样式名称"""
layout_type: str = "column" layout_type: str = "column"
"""布局类型 (如 'column', 'row', 'grid')"""
children: list[LayoutItem] = Field( children: list[LayoutItem] = Field(
default_factory=list, description="要布局的项目列表" default_factory=list, description="要布局的项目列表"
) )
"""要布局的项目列表"""
options: dict[str, Any] = Field( options: dict[str, Any] = Field(
default_factory=dict, description="传递给模板的选项" default_factory=dict, description="传递给模板的选项"
) )
"""传递给模板的选项"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -12,6 +12,7 @@ class ListItem(BaseModel):
"""列表中的单个项目,其内容可以是任何可渲染组件。""" """列表中的单个项目,其内容可以是任何可渲染组件。"""
component: RenderableComponent = Field(..., description="要渲染的组件的数据模型") component: RenderableComponent = Field(..., description="要渲染的组件的数据模型")
"""要渲染的组件的数据模型"""
class ListData(ContainerComponent): class ListData(ContainerComponent):
@ -19,7 +20,9 @@ class ListData(ContainerComponent):
component_type: Literal["list"] = "list" component_type: Literal["list"] = "list"
items: list[ListItem] = Field(default_factory=list, description="列表项目") items: list[ListItem] = Field(default_factory=list, description="列表项目")
"""列表项目"""
ordered: bool = Field(default=False, description="是否为有序列表") ordered: bool = Field(default=False, description="是否为有序列表")
"""是否为有序列表"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -44,7 +44,9 @@ class TextElement(MarkdownElement):
class HeadingElement(MarkdownElement): class HeadingElement(MarkdownElement):
type: Literal["heading"] = "heading" type: Literal["heading"] = "heading"
text: str text: str
level: int = Field(..., ge=1, le=6) """标题文本"""
level: int = Field(..., ge=1, le=6, description="标题级别 (1-6)")
"""标题级别 (1-6)"""
def to_markdown(self) -> str: def to_markdown(self) -> str:
return f"{'#' * self.level} {self.text}" return f"{'#' * self.level} {self.text}"
@ -53,7 +55,9 @@ class HeadingElement(MarkdownElement):
class ImageElement(MarkdownElement): class ImageElement(MarkdownElement):
type: Literal["image"] = "image" type: Literal["image"] = "image"
src: str src: str
"""图片来源 (URL或data URI)"""
alt: str = "image" alt: str = "image"
"""图片的替代文本"""
def to_markdown(self) -> str: def to_markdown(self) -> str:
return f"![{self.alt}]({self.src})" return f"![{self.alt}]({self.src})"
@ -62,7 +66,9 @@ class ImageElement(MarkdownElement):
class CodeElement(MarkdownElement): class CodeElement(MarkdownElement):
type: Literal["code"] = "code" type: Literal["code"] = "code"
code: str code: str
"""代码字符串"""
language: str = "" language: str = ""
"""代码语言,用于语法高亮"""
def to_markdown(self) -> str: def to_markdown(self) -> str:
return f"```{self.language}\n{self.code}\n```" return f"```{self.language}\n{self.code}\n```"
@ -71,6 +77,7 @@ class CodeElement(MarkdownElement):
class RawHtmlElement(MarkdownElement): class RawHtmlElement(MarkdownElement):
type: Literal["raw_html"] = "raw_html" type: Literal["raw_html"] = "raw_html"
html: str html: str
"""原始HTML字符串"""
def to_markdown(self) -> str: def to_markdown(self) -> str:
return self.html return self.html
@ -79,8 +86,11 @@ class RawHtmlElement(MarkdownElement):
class TableElement(MarkdownElement): class TableElement(MarkdownElement):
type: Literal["table"] = "table" type: Literal["table"] = "table"
headers: list[str] headers: list[str]
"""表格的表头列表"""
rows: list[list[str]] rows: list[list[str]]
"""表格的数据行列表"""
alignments: list[Literal["left", "center", "right"]] | None = None alignments: list[Literal["left", "center", "right"]] | None = None
"""每列的对齐方式"""
def to_markdown(self) -> str: def to_markdown(self) -> str:
header_row = "| " + " | ".join(self.headers) + " |" header_row = "| " + " | ".join(self.headers) + " |"
@ -102,7 +112,10 @@ class TableElement(MarkdownElement):
class ContainerElement(MarkdownElement): class ContainerElement(MarkdownElement):
content: list[MarkdownElement] = Field(default_factory=list) content: list[MarkdownElement] = Field(
default_factory=list, description="容器内包含的Markdown元素列表"
)
"""容器内包含的Markdown元素列表"""
class QuoteElement(ContainerElement): class QuoteElement(ContainerElement):
@ -121,6 +134,7 @@ class ListItemElement(ContainerElement):
class ListElement(ContainerElement): class ListElement(ContainerElement):
type: Literal["list"] = "list" type: Literal["list"] = "list"
ordered: bool = False ordered: bool = False
"""是否为有序列表 (例如 1., 2.)"""
def to_markdown(self) -> str: def to_markdown(self) -> str:
lines = [] lines = []
@ -137,6 +151,7 @@ class ComponentElement(MarkdownElement):
type: Literal["component"] = "component" type: Literal["component"] = "component"
component: RenderableComponent component: RenderableComponent
"""嵌入在Markdown中的可渲染组件"""
def to_markdown(self) -> str: def to_markdown(self) -> str:
return "" return ""
@ -146,9 +161,15 @@ class MarkdownData(ContainerComponent):
"""Markdown转图片的数据模型""" """Markdown转图片的数据模型"""
style_name: str | None = None style_name: str | None = None
elements: list[MarkdownElement] = Field(default_factory=list) """Markdown内容的样式名称"""
elements: list[MarkdownElement] = Field(
default_factory=list, description="构成Markdown文档的元素列表"
)
"""构成Markdown文档的元素列表"""
width: int = 800 width: int = 800
"""最终渲染图片的宽度"""
css_path: str | None = None css_path: str | None = None
"""自定义CSS文件的绝对路径"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:
@ -180,7 +201,6 @@ class MarkdownData(ContainerComponent):
logger.warning(f"Markdown自定义CSS文件不存在: {self.css_path}") logger.warning(f"Markdown自定义CSS文件不存在: {self.css_path}")
else: else:
style_name = self.style_name or "light" style_name = self.style_name or "light"
# 使用上下文对象来解析路径
css_path = await context.theme_manager.resolve_markdown_style_path( css_path = await context.theme_manager.resolve_markdown_style_path(
style_name, context style_name, context
) )

View File

@ -22,21 +22,32 @@ class NotebookElement(BaseModel):
"component", "component",
] ]
text: str | None = None text: str | None = None
"""元素的文本内容 (用于标题、段落、引用)"""
level: int | None = None level: int | None = None
"""标题的级别 (1-4)"""
src: str | None = None src: str | None = None
"""图片的来源 (URL或data URI)"""
caption: str | None = None caption: str | None = None
"""图片的说明文字"""
code: str | None = None code: str | None = None
"""代码块的内容"""
language: str | None = None language: str | None = None
"""代码块的语言"""
data: list[str] | None = None data: list[str] | None = None
"""列表项的内容列表"""
ordered: bool | None = None ordered: bool | None = None
"""是否为有序列表"""
component: RenderableComponent | None = None component: RenderableComponent | None = None
"""嵌入的自定义可渲染组件"""
class NotebookData(ContainerComponent): class NotebookData(ContainerComponent):
"""Notebook转图片的数据模型""" """Notebook转图片的数据模型"""
style_name: str | None = None style_name: str | None = None
"""Notebook的样式名称"""
elements: list[NotebookElement] elements: list[NotebookElement]
"""构成Notebook页面的元素列表"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -1,8 +1,6 @@
from pathlib import Path
from typing import Literal from typing import Literal
from nonebot.compat import field_validator from pydantic import BaseModel, Field
from pydantic import BaseModel
from ...models.components.progress_bar import ProgressBar from ...models.components.progress_bar import ProgressBar
from .base import RenderableComponent from .base import RenderableComponent
@ -10,6 +8,7 @@ from .text import TextSpan
__all__ = [ __all__ = [
"BaseCell", "BaseCell",
"ComponentCell",
"ImageCell", "ImageCell",
"ProgressBarCell", "ProgressBarCell",
"RichTextCell", "RichTextCell",
@ -30,7 +29,7 @@ class TextCell(BaseCell):
"""文本单元格""" """文本单元格"""
type: Literal["text"] = "text" # type: ignore type: Literal["text"] = "text" # type: ignore
content: str | float content: str
bold: bool = False bold: bool = False
color: str | None = None color: str | None = None
@ -39,18 +38,12 @@ class ImageCell(BaseCell):
"""图片单元格""" """图片单元格"""
type: Literal["image"] = "image" # type: ignore type: Literal["image"] = "image" # type: ignore
src: str | Path src: str
width: int = 40 width: int = 40
height: int = 40 height: int = 40
shape: Literal["square", "circle"] = "square" shape: Literal["square", "circle"] = "square"
alt: str = "image" alt: str = "image"
@field_validator("src", mode="before")
def validate_src(cls, v: str) -> str:
if isinstance(v, Path):
v = v.resolve().as_uri()
return v
class StatusBadgeCell(BaseCell): class StatusBadgeCell(BaseCell):
"""状态徽章单元格""" """状态徽章单元格"""
@ -70,20 +63,28 @@ class RichTextCell(BaseCell):
"""富文本单元格,支持多个带样式的文本片段""" """富文本单元格,支持多个带样式的文本片段"""
type: Literal["rich_text"] = "rich_text" # type: ignore type: Literal["rich_text"] = "rich_text" # type: ignore
spans: list[TextSpan] = [] spans: list[TextSpan] = Field(default_factory=list, description="文本片段列表")
"""文本片段列表""" """文本片段列表"""
direction: Literal["column", "row"] = "column" direction: Literal["column", "row"] = Field("column", description="片段排列方向")
"""片段排列方向""" """片段排列方向"""
gap: str = "4px" gap: str = Field("4px", description="片段之间的间距")
"""片段之间的间距""" """片段之间的间距"""
class ComponentCell(BaseCell):
"""一个通用的单元格,可以容纳任何可渲染的组件。"""
type: str = "component"
component: RenderableComponent
TableCell = ( TableCell = (
TextCell TextCell
| ImageCell | ImageCell
| StatusBadgeCell | StatusBadgeCell
| ProgressBarCell | ProgressBarCell
| RichTextCell | RichTextCell
| ComponentCell
| str | str
| int | int
| float | float
@ -95,17 +96,22 @@ class TableData(RenderableComponent):
"""通用表格的数据模型""" """通用表格的数据模型"""
style_name: str | None = None style_name: str | None = None
title: str """应用于表格容器的样式名称"""
title: str = Field(..., description="表格主标题")
"""表格主标题""" """表格主标题"""
tip: str | None = None tip: str | None = Field(None, description="表格下方的提示信息")
"""表格下方的提示信息""" """表格下方的提示信息"""
headers: list[str] = [] # noqa: RUF012 headers: list[str] = Field(default_factory=list, description="表头列表")
"""表头列表""" """表头列表"""
rows: list[list[TableCell]] = [] # noqa: RUF012 rows: list[list[TableCell]] = Field(default_factory=list, description="数据行列表")
"""数据行列表""" """数据行列表"""
column_alignments: list[Literal["left", "center", "right"]] | None = None column_alignments: list[Literal["left", "center", "right"]] | None = Field(
default=None, description="每列的对齐方式"
)
"""每列的对齐方式""" """每列的对齐方式"""
column_widths: list[str | int] | None = None column_widths: list[str | int] | None = Field(
default=None, description="每列的宽度 (e.g., ['50px', 'auto', 100])"
)
"""每列的宽度 (e.g., ['50px', 'auto', 100])""" """每列的宽度 (e.g., ['50px', 'auto', 100])"""
@property @property

View File

@ -1,6 +1,8 @@
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from pydantic import Field
from .base import RenderableComponent from .base import RenderableComponent
__all__ = ["TemplateComponent"] __all__ = ["TemplateComponent"]
@ -10,8 +12,11 @@ class TemplateComponent(RenderableComponent):
"""基于独立模板文件的UI组件""" """基于独立模板文件的UI组件"""
_is_standalone_template: bool = True _is_standalone_template: bool = True
template_path: str | Path """标记此组件为独立模板"""
data: dict[str, Any] template_path: str | Path = Field(..., description="指向HTML模板文件的路径")
"""指向HTML模板文件的路径"""
data: dict[str, Any] = Field(..., description="传递给模板的上下文数据字典")
"""传递给模板的上下文数据字典"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -23,9 +23,11 @@ class TextData(RenderableComponent):
"""轻量级富文本组件的数据模型""" """轻量级富文本组件的数据模型"""
spans: list[TextSpan] = Field(default_factory=list, description="文本片段列表") spans: list[TextSpan] = Field(default_factory=list, description="文本片段列表")
"""文本片段列表"""
align: Literal["left", "right", "center"] = Field( align: Literal["left", "right", "center"] = Field(
"left", description="整体文本对齐方式" "left", description="整体文本对齐方式"
) )
"""整体文本对齐方式"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -13,25 +13,35 @@ class HelpItem(BaseModel):
"""帮助菜单中的单个功能项""" """帮助菜单中的单个功能项"""
name: str name: str
"""功能名称"""
description: str description: str
"""功能描述"""
usage: str usage: str
"""功能用法说明"""
class HelpCategory(BaseModel): class HelpCategory(BaseModel):
"""帮助菜单中的一个功能类别""" """帮助菜单中的一个功能类别"""
title: str title: str
"""分类标题"""
icon_svg_path: str icon_svg_path: str
"""分类图标的SVG路径数据"""
items: list[HelpItem] items: list[HelpItem]
"""该分类下的功能项列表"""
class PluginHelpPageData(RenderableComponent): class PluginHelpPageData(RenderableComponent):
"""通用插件帮助页面的数据模型""" """通用插件帮助页面的数据模型"""
style_name: str | None = None style_name: str | None = None
"""页面样式名称"""
bot_nickname: str bot_nickname: str
"""机器人昵称"""
page_title: str page_title: str
"""页面主标题"""
categories: list[HelpCategory] categories: list[HelpCategory]
"""帮助分类列表"""
@property @property
def template_name(self) -> str: def template_name(self) -> str:

View File

@ -13,29 +13,43 @@ class PluginMenuItem(BaseModel):
"""插件菜单中的单个插件项""" """插件菜单中的单个插件项"""
id: str id: str
"""插件的唯一ID"""
name: str name: str
"""插件名称"""
status: bool status: bool
"""插件在当前群组的开关状态"""
has_superuser_help: bool has_superuser_help: bool
commands: list[str] = Field(default_factory=list) """插件是否有超级用户专属帮助"""
commands: list[str] = Field(default_factory=list, description="插件的主要命令列表")
"""插件的主要命令列表"""
class PluginMenuCategory(BaseModel): class PluginMenuCategory(BaseModel):
"""插件菜单中的一个分类""" """插件菜单中的一个分类"""
name: str name: str
items: list[PluginMenuItem] """插件分类名称"""
items: list[PluginMenuItem] = Field(..., description="该分类下的插件项列表")
"""该分类下的插件项列表"""
class PluginMenuData(RenderableComponent): class PluginMenuData(RenderableComponent):
"""通用插件帮助菜单的数据模型""" """通用插件帮助菜单的数据模型"""
style_name: str | None = None style_name: str | None = None
"""页面样式名称"""
bot_name: str bot_name: str
"""机器人名称"""
bot_avatar_url: str bot_avatar_url: str
"""机器人头像URL"""
is_detail: bool is_detail: bool
"""是否为详细菜单模式"""
plugin_count: int plugin_count: int
"""总插件数量"""
active_count: int active_count: int
"""已启用插件数量"""
categories: list[PluginMenuCategory] categories: list[PluginMenuCategory]
"""插件分类列表"""
@property @property
def template_name(self) -> str: def template_name(self) -> str: