群总结模板升级:新增Gemini风格卡片并优化Markdown富标签渲染

变更项:

1. 新增 templates/gemini_summary_card.html,按 Gemini 风格实现移动卡片化总结模板。

2. message_summary 渲染链路升级:支持更完整的 Markdown 富标签转 HTML(标题/列表/表格/代码块/引用等)。

3. 增加渲染后 HTML 安全过滤,清理 script/iframe/on* 事件与 javascript: 链接。

4. 增加 markdown 依赖缺失时的轻量回退解析,保证插件在最小环境下可用。

5. 默认输出配置切换为 template,并指向新 Gemini 风格模板。
This commit is contained in:
liuwei
2026-04-23 09:37:31 +08:00
parent f438f0f955
commit 35f1fbc978
4 changed files with 576 additions and 8 deletions

View File

@@ -9,6 +9,11 @@ from typing import Dict, Any, Tuple, Optional, List
from loguru import logger
from markupsafe import Markup
try:
# 优先使用 markdown 库做完整渲染(支持表格、代码块等)。
import markdown as markdown_lib
except Exception:
markdown_lib = None
from base.plugin_common.message_plugin_interface import MessagePluginInterface
from base.plugin_common.plugin_interface import PluginStatus
@@ -410,11 +415,76 @@ class MessageSummaryPlugin(MessagePluginInterface):
return cleaned
@staticmethod
def _summary_markdown_to_html(summary_text: str) -> str:
"""把总结 Markdown 转为基础 HTML 片段(模板内部展示用)。"""
# 这里不依赖第三方 markdown 库,保证在最小运行环境也能稳定渲染。
# 规则按“标题/列表/段落”三类做轻量转换,足够覆盖总结文本场景。
lines = str(summary_text or "").splitlines()
def _sanitize_rendered_html(rendered_html: str) -> str:
"""对渲染后的 HTML 做最小安全过滤。
安全策略:
1. 移除 script/style/iframe 等高风险标签,避免模板渲染执行脚本;
2. 清除行内事件属性onload/onerror/onclick...
3. 禁止 javascript: 协议链接。
说明:
- 这里是“轻量过滤”,目标是平衡安全与展示完整度;
- 若后续需要更严格过滤,可接入专门的 HTML Sanitizer。
"""
safe_html = str(rendered_html or "")
# 删除高风险标签及其内容。
safe_html = re.sub(
r"<\s*(script|style|iframe|object|embed|form|link|meta)\b[^>]*>.*?<\s*/\s*\1\s*>",
"",
safe_html,
flags=re.IGNORECASE | re.DOTALL,
)
# 删除自闭合高风险标签。
safe_html = re.sub(
r"<\s*(script|style|iframe|object|embed|form|link|meta)\b[^>]*/\s*>",
"",
safe_html,
flags=re.IGNORECASE | re.DOTALL,
)
# 删除行内事件处理器属性。
safe_html = re.sub(r"\son[a-zA-Z]+\s*=\s*(['\"]).*?\1", "", safe_html, flags=re.IGNORECASE | re.DOTALL)
# 阻断 javascript: 链接。
safe_html = re.sub(
r"""(href|src)\s*=\s*(['"])\s*javascript:[^'"]*\2""",
r'\1=\2#\2',
safe_html,
flags=re.IGNORECASE,
)
return safe_html
@classmethod
def _summary_markdown_to_html(cls, summary_text: str) -> str:
"""把总结 Markdown 转为 HTML 片段(模板内部展示用)。
升级点:
1. 使用 markdown 库完整支持标题、列表、粗斜体、引用、代码块、表格等结构;
2. 对 LLM 输出里常见的富标签 markdown例如 ```、|表格|、> 引用)效果更好;
3. 渲染后做一次轻量安全过滤,避免模板内注入脚本。
"""
text = str(summary_text or "").strip()
if not text:
return "<p>暂无总结内容。</p>"
# 兼容处理:
# 1. 环境安装了 markdown 库时,走完整渲染;
# 2. 未安装时自动降级到内置轻量转换,避免插件启动失败。
if markdown_lib is not None:
rendered = markdown_lib.markdown(
text,
extensions=[
"extra", # 综合扩展:支持表格、定义列表等
"fenced_code", # 支持 ``` 代码块
"tables", # 支持 Markdown 表格
"sane_lists", # 更稳定的列表解析
"nl2br", # 保留换行,提升聊天总结可读性
],
output_format="html5",
)
return cls._sanitize_rendered_html(rendered)
# 轻量回退实现(兼容无 markdown 三方包的运行环境)。
lines = text.splitlines()
html_parts: List[str] = []
list_items: List[str] = []
@@ -443,7 +513,8 @@ class MessageSummaryPlugin(MessagePluginInterface):
flush_list()
html_parts.append(f"<p>{html.escape(line)}</p>")
flush_list()
return "".join(html_parts)
rendered = "".join(html_parts)
return cls._sanitize_rendered_html(rendered)
def _render_summary_template_html(self, group_name: str, summary_text: str) -> str:
"""根据模板路径渲染总结图片 HTML。"""