模板化改造斗鱼日报与群聊总结图片渲染,支持HTML模板独立维护
变更项:\n1. 将 plugins/douyu/report_template.py 从内联HTML重构为模板渲染,新增 plugins/douyu/templates/daily_report.html 承载完整样式与结构,Python侧仅保留数据组装与安全注入。\n2. 修复斗鱼日报模板迁移后的样式缺失问题,补齐 metric-card、insight-card、badge-wall、active-user-grid、chart 等所有关键类样式,确保视觉与旧版一致。\n3. 在 plugins/message_summary/main.py 新增模板化图片渲染链路:优先使用 HtmlTemplateRenderer + html_to_image 生成总结图片,模板异常时自动回退 convert_md_str_to_image,保证稳定性。\n4. 新增 plugins/message_summary/templates/summary_card.html 作为群聊总结卡片模板,后续可仅改模板文件完成UI迭代。\n5. 扩展 plugins/message_summary/config.toml 输出配置,增加 summary_image_mode 与 summary_image_template_path,支持模板模式与回退模式按配置切换。\n6. 保持原有业务流程与发送逻辑不变,仅改造渲染层,降低后续维护成本。
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
@@ -7,6 +8,7 @@ from pathlib import Path
|
||||
from typing import Dict, Any, Tuple, Optional, List
|
||||
|
||||
from loguru import logger
|
||||
from markupsafe import Markup
|
||||
|
||||
from base.plugin_common.message_plugin_interface import MessagePluginInterface
|
||||
from base.plugin_common.plugin_interface import PluginStatus
|
||||
@@ -15,7 +17,8 @@ from utils.compress_chat_data import compress_chat_data
|
||||
from utils.decorator.plugin_decorators import plugin_stats_decorator
|
||||
from utils.decorator.points_decorator import plugin_points_cost
|
||||
from utils.decorator.rate_limit_decorator import group_feature_rate_limit
|
||||
from utils.markdown_to_image import convert_md_str_to_image
|
||||
from utils.html_template_renderer import HtmlTemplateRenderer
|
||||
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 GroupBotManager, PermissionStatus
|
||||
from utils.string_utils import remove_reasoning_content, remove_trailing_content
|
||||
@@ -94,6 +97,13 @@ class MessageSummaryPlugin(MessagePluginInterface):
|
||||
self._image_render_timeout_seconds = int(output_config.get("image_render_timeout_seconds", 90))
|
||||
# 默认只尝试 1 次,优先保证任务快速返回;需要更高成功率可在配置里提高。
|
||||
self._image_render_retries = int(output_config.get("image_render_retries", 1))
|
||||
# 输出模板配置:
|
||||
# 1. summary_image_mode=template 时优先按 HTML 模板生图;
|
||||
# 2. template 失败会自动回退到 markdown 模式,保证可用性。
|
||||
self._summary_image_mode = str(output_config.get("summary_image_mode", "template")).strip().lower()
|
||||
self._summary_image_template_path = str(
|
||||
output_config.get("summary_image_template_path", "plugins/message_summary/templates/summary_card.html")
|
||||
).strip()
|
||||
self.llm_client = UnifiedLLMClient(api_config)
|
||||
self._api_mode = self.llm_client.mode or self._api_mode
|
||||
self._response_mode = self.llm_client.response_mode or self._response_mode
|
||||
@@ -398,6 +408,91 @@ class MessageSummaryPlugin(MessagePluginInterface):
|
||||
cleaned = re.sub(r'\n{3,}', '\n\n', cleaned).strip()
|
||||
return cleaned
|
||||
|
||||
@staticmethod
|
||||
def _summary_markdown_to_html(summary_text: str) -> str:
|
||||
"""把总结 Markdown 转为基础 HTML 片段(模板内部展示用)。"""
|
||||
# 这里不依赖第三方 markdown 库,保证在最小运行环境也能稳定渲染。
|
||||
# 规则按“标题/列表/段落”三类做轻量转换,足够覆盖总结文本场景。
|
||||
lines = str(summary_text or "").splitlines()
|
||||
html_parts: List[str] = []
|
||||
list_items: List[str] = []
|
||||
|
||||
def flush_list() -> None:
|
||||
if not list_items:
|
||||
return
|
||||
html_parts.append("<ul>" + "".join(list_items) + "</ul>")
|
||||
list_items.clear()
|
||||
|
||||
for raw_line in lines:
|
||||
line = raw_line.strip()
|
||||
if not line:
|
||||
flush_list()
|
||||
continue
|
||||
if line.startswith("## "):
|
||||
flush_list()
|
||||
html_parts.append(f"<h2>{html.escape(line[3:].strip())}</h2>")
|
||||
continue
|
||||
if line.startswith("### "):
|
||||
flush_list()
|
||||
html_parts.append(f"<h3>{html.escape(line[4:].strip())}</h3>")
|
||||
continue
|
||||
if line.startswith("- "):
|
||||
list_items.append(f"<li>{html.escape(line[2:].strip())}</li>")
|
||||
continue
|
||||
flush_list()
|
||||
html_parts.append(f"<p>{html.escape(line)}</p>")
|
||||
flush_list()
|
||||
return "".join(html_parts)
|
||||
|
||||
def _render_summary_template_html(self, group_name: str, summary_text: str) -> str:
|
||||
"""根据模板路径渲染总结图片 HTML。"""
|
||||
# 约束:模板只负责展示,正文仍然由模型生成并在此做安全转义后注入。
|
||||
renderer = HtmlTemplateRenderer()
|
||||
summary_html = self._summary_markdown_to_html(summary_text)
|
||||
return renderer.render(
|
||||
self._summary_image_template_path,
|
||||
{
|
||||
"title": f"{group_name} 群聊总结",
|
||||
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"summary_html": Markup(summary_html),
|
||||
},
|
||||
)
|
||||
|
||||
async def _render_summary_image(
|
||||
self,
|
||||
answer: str,
|
||||
group_name: str,
|
||||
output_filename: str,
|
||||
total_timeout: int,
|
||||
) -> str:
|
||||
"""生成总结图片:模板优先,失败自动回退 Markdown。"""
|
||||
image_root = Path("temp") / "md2image"
|
||||
image_root.mkdir(parents=True, exist_ok=True)
|
||||
output_path = image_root / output_filename
|
||||
|
||||
# 优先模板渲染,便于后续仅改 HTML 文件完成样式迭代。
|
||||
if self._summary_image_mode != "markdown":
|
||||
try:
|
||||
html_content = self._render_summary_template_html(group_name=group_name, summary_text=answer)
|
||||
await asyncio.wait_for(html_to_image(html_content, str(output_path)), timeout=total_timeout)
|
||||
if not output_path.exists() or output_path.stat().st_size < 1024:
|
||||
raise RuntimeError("模板截图输出文件异常")
|
||||
return str(output_path.resolve())
|
||||
except Exception as template_error:
|
||||
self.LOG.warning(f"模板渲染失败,回退 Markdown 转图: {template_error}")
|
||||
|
||||
# 回退逻辑:保持旧链路,确保模板异常时仍能发送图片。
|
||||
return await asyncio.wait_for(
|
||||
convert_md_str_to_image(
|
||||
answer,
|
||||
output_filename,
|
||||
max_retries=self._image_render_retries,
|
||||
render_timeout_seconds=self._image_render_timeout_seconds,
|
||||
html_timeout_seconds=min(30, self._image_render_timeout_seconds),
|
||||
),
|
||||
timeout=total_timeout,
|
||||
)
|
||||
|
||||
def _stringify_output(self, value: Any) -> str:
|
||||
"""把 workflow 输出统一转成文本"""
|
||||
if value is None:
|
||||
@@ -546,15 +641,12 @@ class MessageSummaryPlugin(MessagePluginInterface):
|
||||
self.LOG.info(f"开始生成图片: {output_path}")
|
||||
# 额外包一层总超时,确保就算底层依赖异常也不会把整个任务拖住。
|
||||
total_timeout = max(30, self._image_render_timeout_seconds * self._image_render_retries + 10)
|
||||
spath = await asyncio.wait_for(
|
||||
convert_md_str_to_image(
|
||||
answer,
|
||||
output_path,
|
||||
max_retries=self._image_render_retries,
|
||||
render_timeout_seconds=self._image_render_timeout_seconds,
|
||||
html_timeout_seconds=min(30, self._image_render_timeout_seconds),
|
||||
),
|
||||
timeout=total_timeout,
|
||||
# 统一走图片渲染入口:优先模板,失败自动降级到旧的 Markdown 转图链路。
|
||||
spath = await self._render_summary_image(
|
||||
answer=answer,
|
||||
group_name=group_name,
|
||||
output_filename=output_path,
|
||||
total_timeout=total_timeout,
|
||||
)
|
||||
self.LOG.info(f"成功生成图片: {spath}")
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user