From f1a6f6d5652d3db3ca7a0484a8b70b2e54154f22 Mon Sep 17 00:00:00 2001 From: liuwei Date: Mon, 20 Apr 2026 13:12:44 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=80=9A=E7=94=A8HTML?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E6=B8=B2=E6=9F=93=E5=99=A8=E5=B9=B6=E5=B0=86?= =?UTF-8?q?=E6=9C=BA=E5=99=A8=E4=BA=BA=E8=8F=9C=E5=8D=95=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E9=A9=B1=E5=8A=A8=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/robot_menu/config.toml | 3 + plugins/robot_menu/main.py | 9 + plugins/robot_menu/menu_render_tool.py | 284 +++---------------- plugins/robot_menu/templates/menu_cards.html | 234 +++++++++++++++ utils/html_template_renderer.py | 42 +++ 5 files changed, 327 insertions(+), 245 deletions(-) create mode 100644 plugins/robot_menu/templates/menu_cards.html create mode 100644 utils/html_template_renderer.py diff --git a/plugins/robot_menu/config.toml b/plugins/robot_menu/config.toml index 71644f8..0e08ac3 100644 --- a/plugins/robot_menu/config.toml +++ b/plugins/robot_menu/config.toml @@ -12,6 +12,9 @@ image_fallback_to_text = false # md2image 渲染参数:可按服务器性能调整 image_render_timeout_seconds = 45 image_render_retries = 1 +# 菜单图片 HTML 模板路径(相对仓库根目录) +# 后续 UI 改版只需要改模板文件,无需改 Python 代码 +image_template_path = "plugins/robot_menu/templates/menu_cards.html" command-format = """ 📝功能菜单指令: 菜单 - 显示功能菜单 diff --git a/plugins/robot_menu/main.py b/plugins/robot_menu/main.py index 3d52b6e..1a75b33 100644 --- a/plugins/robot_menu/main.py +++ b/plugins/robot_menu/main.py @@ -87,12 +87,21 @@ class RobotMenuPlugin(MessagePluginInterface): self.image_render_retries = int( self._config.get("RobotMenu", {}).get("image_render_retries", 1) ) + # 菜单图片模板文件路径(相对仓库根目录): + # 调整样式和布局时只改模板,不改 Python 逻辑。 + self.image_template_path = str( + self._config.get("RobotMenu", {}).get( + "image_template_path", + "plugins/robot_menu/templates/menu_cards.html", + ) + ).strip() or "plugins/robot_menu/templates/menu_cards.html" # 初始化“菜单渲染工具”,后续菜单图片与文本发送统一由该工具负责。 self.menu_renderer = RobotMenuRenderTool( output_mode=output_mode, image_fallback_to_text=self.image_fallback_to_text, image_render_timeout_seconds=self.image_render_timeout_seconds, image_render_retries=self.image_render_retries, + image_template_path=self.image_template_path, log=self.LOG, ) diff --git a/plugins/robot_menu/menu_render_tool.py b/plugins/robot_menu/menu_render_tool.py index 9b3aea1..bdb6b0e 100644 --- a/plugins/robot_menu/menu_render_tool.py +++ b/plugins/robot_menu/menu_render_tool.py @@ -1,5 +1,4 @@ import asyncio -import html import os import re import time @@ -11,6 +10,7 @@ from loguru import logger as default_logger from utils.markdown_to_image import convert_md_str_to_image, html_to_image from utils.revoke.message_auto_revoke import MessageAutoRevoke from utils.robot_cmd.robot_command import Feature, GroupBotManager, PermissionStatus +from utils.html_template_renderer import HtmlTemplateRenderer from wechat_ipad import WechatAPIClient @@ -29,6 +29,7 @@ class RobotMenuRenderTool: image_fallback_to_text: bool, image_render_timeout_seconds: int, image_render_retries: int, + image_template_path: str, log=default_logger, ): # 输出模式:支持 text / image,非法值自动归一化为 text。 @@ -40,6 +41,9 @@ class RobotMenuRenderTool: self.image_render_retries = int(image_render_retries) # 注入日志对象,便于主插件统一控制日志风格与输出目标。 self.log = log or default_logger + # 菜单图片模板路径(相对仓库根目录),支持仅改模板文件完成 UI 更新。 + self.image_template_path = str(image_template_path or "").strip() or "plugins/robot_menu/templates/menu_cards.html" + self.template_renderer = HtmlTemplateRenderer() @staticmethod def normalize_output_mode(raw_mode: Any) -> str: @@ -132,268 +136,58 @@ class RobotMenuRenderTool: lines.append("") return "\n".join(lines) - def _get_feature_command_examples(self, feature: Feature) -> str: - """返回单个功能在卡片中的紧凑使用说明(不包含管理员内容)。""" + def _build_feature_command_payload(self, feature: Feature) -> dict: + """构建功能卡片中的命令展示数据。""" _, cmd_hint = self._split_feature_description(feature.description) tokens = self._extract_command_tokens(cmd_hint) if not tokens: - return "自动/定时触发,无需发送命令" - - rows = [f"指令:{html.escape(tokens[0])}"] - if len(tokens) > 1: - alias_text = " / ".join([f"{html.escape(t)}" for t in tokens[1:4]]) - rows.append(f"别名:{alias_text}") - return "
".join(rows) + return { + "is_auto": True, + "primary": "", + "aliases": [], + "tip": "自动/定时触发,无需发送命令", + } + return { + "is_auto": False, + "primary": tokens[0], + "aliases": tokens[1:4], + "tip": "", + } def build_feature_status_html(self, group_id: str) -> str: - """构建 PlayStation 风格菜单 HTML(黑-白-蓝三段式)。""" - # 设计实现说明(基于 DESIGN-playstation.md): - # 1. 三段式表面:黑色英雄区 -> 白色内容区 -> 蓝色底部; - # 2. 主色锚点固定为 PlayStation Blue #0070cc,交互强调色为 #1eaedb; - # 3. 组件采用圆角胶囊/卡片体系,保持信息密度与可读性平衡。 + """基于外部 HTML 模板构建菜单页面。""" user_features = self._iter_user_command_features() feature_cards = [] enabled_count = 0 for feature in user_features: title, _ = self._split_feature_description(feature.description) - command_examples = self._get_feature_command_examples(feature) status = GroupBotManager.get_group_permission(group_id, feature) is_enabled = status == PermissionStatus.ENABLED if is_enabled: enabled_count += 1 - status_text = "开启" if is_enabled else "关闭" - status_class = "status-on" if is_enabled else "status-off" + command_payload = self._build_feature_command_payload(feature) feature_cards.append( - f""" -
-
-
{feature.value:02d}
-
-

{html.escape(title)}

-

功能键:{html.escape(feature.name)}

-
- {status_text} -
-
-

{command_examples}

-
-
- """ + { + "index": int(feature.value), + "title": title, + "feature_key": str(feature.name), + "status_text": "开启" if is_enabled else "关闭", + "status_class": "status-on" if is_enabled else "status-off", + "command": command_payload, + } ) now_text = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) - return f""" - - - - - - - -
-
-

机器人功能菜单

-

用户功能查看中心 · 直达命令与状态一屏可见

-
- 目标:{html.escape(group_id)} - 功能总数:{len(user_features)} - 已开启:{enabled_count} - 生成时间:{now_text} -
-
-
-
-

功能卡片

-
- {''.join(feature_cards)} -
-
-
-
- PlayStation Blue UI · 功能查看体验重构 - 菜单 -
-
- - - """ + return self.template_renderer.render( + self.image_template_path, + { + "group_id": str(group_id or ""), + "now_text": now_text, + "feature_total": len(user_features), + "enabled_count": enabled_count, + "feature_cards": feature_cards, + }, + ) async def send_menu_content( self, diff --git a/plugins/robot_menu/templates/menu_cards.html b/plugins/robot_menu/templates/menu_cards.html new file mode 100644 index 0000000..ea68cf1 --- /dev/null +++ b/plugins/robot_menu/templates/menu_cards.html @@ -0,0 +1,234 @@ + + + + + + + +
+
+

机器人功能菜单

+

用户功能查看中心 · 直达命令与状态一屏可见

+
+ 目标:{{ group_id }} + 功能总数:{{ feature_total }} + 已开启:{{ enabled_count }} + 生成时间:{{ now_text }} +
+
+
+
+

功能卡片

+
+ {% for item in feature_cards %} +
+
+
{{ "%02d"|format(item.index) }}
+
+

{{ item.title }}

+

功能键:{{ item.feature_key }}

+
+ {{ item.status_text }} +
+
+ {% if item.command.is_auto %} +

{{ item.command.tip }}

+ {% else %} +

+ 指令:{{ item.command.primary }} + {% if item.command.aliases %} + + 别名: + {% for alias in item.command.aliases %} + {{ alias }}{% if not loop.last %} / {% endif %} + {% endfor %} + + {% endif %} +

+ {% endif %} +
+
+ {% endfor %} +
+
+
+ +
+ + diff --git a/utils/html_template_renderer.py b/utils/html_template_renderer.py new file mode 100644 index 0000000..87ccbee --- /dev/null +++ b/utils/html_template_renderer.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +"""通用 HTML 模板渲染工具。 + +目标: +1. 把页面结构/样式从 Python 代码中抽离到 .html 模板文件; +2. Python 只负责组装数据,模板负责展示; +3. 后续改 UI 时只改模板文件,不改业务代码。 +""" + +from pathlib import Path +from typing import Any, Dict + +from jinja2 import Environment, FileSystemLoader, TemplateNotFound, select_autoescape + + +class HtmlTemplateRenderer: + """基于 Jinja2 的 HTML 模板渲染器。""" + + def __init__(self, template_root: Path = None): + # 默认以仓库根目录作为模板根路径,确保可通过相对路径定位模板文件。 + root = template_root or Path(__file__).resolve().parents[1] + self.template_root = Path(root).resolve() + self.env = Environment( + loader=FileSystemLoader(str(self.template_root)), + autoescape=select_autoescape(enabled_extensions=("html", "xml")), + trim_blocks=True, + lstrip_blocks=True, + ) + + def render(self, template_path: str, context: Dict[str, Any]) -> str: + """渲染模板并返回完整 HTML 字符串。""" + normalized = str(template_path or "").replace("\\", "/").strip() + if not normalized: + raise ValueError("template_path 不能为空") + try: + template = self.env.get_template(normalized) + except TemplateNotFound as e: + raise FileNotFoundError( + f"模板文件不存在: {normalized} (root={self.template_root})" + ) from e + return template.render(**(context or {})) +