From d2187e5545023b7b3acf5ebf388d497e7a7bbf2a Mon Sep 17 00:00:00 2001
From: "Mr.Xia" <1424473282@qq.com>
Date: Thu, 30 Jul 2026 21:21:49 +0800
Subject: [PATCH] =?UTF-8?q?feat(qqpush):=20=E6=B5=8F=E8=A7=88=E5=99=A8?=
=?UTF-8?q?=E9=95=BF=E5=9B=BE=E6=B8=B2=E6=9F=93=E5=85=AC=E5=91=8A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
danding_bot/plugins/danding_qqpush/api.py | 44 ++++++---
.../plugins/danding_qqpush/article_render.py | 89 +++++++++++++++++++
tests/test_danding_qqpush_article_render.py | 44 +++++++++
3 files changed, 165 insertions(+), 12 deletions(-)
create mode 100644 danding_bot/plugins/danding_qqpush/article_render.py
create mode 100644 tests/test_danding_qqpush_article_render.py
diff --git a/danding_bot/plugins/danding_qqpush/api.py b/danding_bot/plugins/danding_qqpush/api.py
index 0ad0f74..845aec1 100644
--- a/danding_bot/plugins/danding_qqpush/api.py
+++ b/danding_bot/plugins/danding_qqpush/api.py
@@ -6,6 +6,7 @@ from typing import Optional
from nonebot import logger
from .config import Config
+from .article_render import render_article_to_base64
from .text_parser import TextParser
from .image_render import ImageRenderer
from .sender import sender
@@ -39,8 +40,17 @@ class PushRequest(BaseModel):
qq: Optional[int] = None
"""可选的被 @ QQ 号;未传时仅发送群消息"""
- text: str
- """通知文本(# 表示换行)"""
+ text: str
+ """通知文本(# 表示换行)"""
+
+ html: Optional[str] = None
+ """可选的完整公告 HTML;传入时使用虚拟浏览器截图"""
+
+ title: Optional[str] = None
+ """公告标题"""
+
+ published_at: Optional[str] = None
+ """公告发布时间"""
# 响应模型
@@ -109,16 +119,26 @@ def create_routes(token: str, config: Config):
parsed_text = text_parser.parse(data.text)
logger.info(f"解析文本: {parsed_text[:50]}..." if len(parsed_text) > 50 else parsed_text)
- # 4. 生成图片 (reuse shared renderer to avoid per-request font loading)
- image_base64 = await asyncio.to_thread(_get_renderer(config).render_to_base64, parsed_text)
- logger.info("图片生成成功")
-
- # 5. 发送消息
- send_result = await sender.send_to_group(
- group_id=data.group_id,
- image_base64=image_base64,
- qq=data.qq,
- )
+ # 4. 原始公告使用 Chromium 截图,普通通知保持既有 Pillow 渲染。
+ if data.html:
+ image_base64 = await render_article_to_base64(
+ data.html,
+ data.title or "蛋定助手通知",
+ data.published_at,
+ )
+ send_result = await sender.send_to_group(
+ group_id=data.group_id,
+ image_base64=image_base64,
+ qq=data.qq,
+ )
+ else:
+ image_base64 = await asyncio.to_thread(_get_renderer(config).render_to_base64, parsed_text)
+ send_result = await sender.send_to_group(
+ group_id=data.group_id,
+ image_base64=image_base64,
+ qq=data.qq,
+ )
+ logger.info("图片生成成功")
if not send_result["success"]:
logger.error(f"消息发送失败: {send_result['error']}")
diff --git a/danding_bot/plugins/danding_qqpush/article_render.py b/danding_bot/plugins/danding_qqpush/article_render.py
new file mode 100644
index 0000000..ad697cd
--- /dev/null
+++ b/danding_bot/plugins/danding_qqpush/article_render.py
@@ -0,0 +1,89 @@
+"""阴阳师等长公告的安全 HTML 截图渲染。"""
+import base64
+import html
+import re
+from typing import Optional
+
+from pyppeteer import launch
+
+try:
+ import bleach
+except ImportError:
+ bleach = None
+
+
+ARTICLE_WIDTH = 900
+ARTICLE_PADDING = 48
+
+_ALLOWED_TAGS = [
+ "p", "br", "strong", "b", "em", "i", "u", "span", "div",
+ "h1", "h2", "h3", "ul", "ol", "li", "blockquote", "hr",
+]
+
+
+def _sanitize_article_html(content_html: str) -> str:
+ """净化外部公告 HTML;缺少 bleach 时退化为安全的纯文本段落。"""
+ if bleach is not None:
+ return bleach.clean(content_html, tags=_ALLOWED_TAGS, attributes={}, strip=True)
+
+ # 生产环境缺少可选依赖时,宁可丢失样式也不把不可信标签交给浏览器执行。
+ text = re.sub(r"(?is)<(script|style).*?>.*?\1>", "", content_html)
+ text = re.sub(r"(?i)
", "\n", text)
+ text = re.sub(r"(?i)(p|div|h[1-6]|li|blockquote)>", "\n", text)
+ text = re.sub(r"<[^>]+>", "", text)
+ return "
" + html.escape(text).replace("\n", "
") + "
第一段
第二段
", + "公告 <标题>", + "2026-07-30 20:27", + ) + + assert "第一段
" in document + assert "第二段" in document + assert "第二段
", "标题", None) + + assert "