From b10ec3493a2d10bd1aeda58591e20c16a38dbacf Mon Sep 17 00:00:00 2001 From: liuwei Date: Mon, 13 Apr 2026 09:54:42 +0800 Subject: [PATCH] feat(message_summary): beautify overview stats pills --- plugins/message_summary/main.py | 14 ++++-- utils/markdown_to_image.py | 85 +++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/plugins/message_summary/main.py b/plugins/message_summary/main.py index dc118d3..e1807f3 100644 --- a/plugins/message_summary/main.py +++ b/plugins/message_summary/main.py @@ -528,11 +528,19 @@ class MessageSummaryPlugin(MessagePluginInterface): if not summary or not summary.strip(): return summary + pairs = [ + ("总", int(stats.get("total_count") or 0)), + ("人数", int(stats.get("participant_count") or 0)), + ("文本", int(stats.get("text_count") or 0)), + ("图片", int(stats.get("image_count") or 0)), + ("视频", int(stats.get("video_count") or 0)), + ("链接", int(stats.get("link_count") or 0)), + ("表情", int(stats.get("emoji_count") or 0)), + ] + stats_line = " · ".join([f"**{label}** {value}" for label, value in pairs]) section_lines = [ "## 群概览", - f"- 📊 总:{int(stats.get('total_count') or 0)} 👥 人:{int(stats.get('participant_count') or 0)}\n" - f"- 💬 文:{int(stats.get('text_count') or 0)} 🖼 图:{int(stats.get('image_count') or 0)} 🎬 视:{int(stats.get('video_count') or 0)}\n" - f"- 🔗 链:{int(stats.get('link_count') or 0)} 😄 表:{int(stats.get('emoji_count') or 0)}" + stats_line, ] section = "\n".join(section_lines) return f"{section}\n\n{summary.strip()}" diff --git a/utils/markdown_to_image.py b/utils/markdown_to_image.py index 8f59952..1d1ca2e 100644 --- a/utils/markdown_to_image.py +++ b/utils/markdown_to_image.py @@ -15,6 +15,43 @@ except ImportError: markdown = None META_KEYWORDS = ["群", "群名", "时间", "日期", "成员", "消息", "统计", "总结", "来源", "生成", "记录"] +STAT_PILL_CLASSES = { + "总": "total", + "人数": "people", + "文本": "text", + "图片": "image", + "视频": "video", + "链接": "link", + "表情": "emoji", +} + + +def _extract_stats_pills_from_markdown(md_content: str) -> str: + text = str(md_content or "") + pattern = re.compile( + r"(^##\s+群概览\s*\n)([^\n]+)(?=\n(?:\n|##\s|###\s|$))", + re.M, + ) + + def replace(match): + stats_line = match.group(2).strip() + parts = [part.strip() for part in stats_line.split("·") if part.strip()] + pills = [] + for part in parts: + item_match = re.match(r"(?:\*\*)?([^*\s]+)(?:\*\*)?\s+(\d+)", part) + if not item_match: + continue + label = item_match.group(1).strip() + value = item_match.group(2).strip() + kind = STAT_PILL_CLASSES.get(label, "default") + pills.append( + f'{label}{value}' + ) + if not pills: + return match.group(0) + return match.group(1) + f'
{"".join(pills)}
' + + return pattern.sub(replace, text, count=1) def _simple_markdown_to_html(md_content: str) -> str: lines = str(md_content or "").splitlines() @@ -58,6 +95,11 @@ def _simple_markdown_to_html(md_content: str) -> str: close_ul() html_parts.append(f"

{stripped[4:].strip()}

") continue + if stripped.startswith("
"): + flush_paragraph() + close_ul() + html_parts.append(stripped) + continue if stripped.startswith("- "): flush_paragraph() if not in_ul: @@ -165,6 +207,7 @@ def _split_hero(html_body: str): async def md_str_to_html_content(md_content): + md_content = _extract_stats_pills_from_markdown(md_content) if markdown is not None: html_body = markdown.markdown(md_content, extensions=['extra', 'codehilite']) else: @@ -354,6 +397,48 @@ async def md_str_to_html_content(md_content): border-radius: 14px; color: #355468; } + .stats-pills { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin: 12px 0 8px; + } + .stats-pill { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 7px 12px; + border-radius: 999px; + font-size: 0.92em; + line-height: 1; + border: 1px solid rgba(148,163,184,0.16); + background: linear-gradient(180deg, rgba(255,255,255,0.96), rgba(248,250,252,0.92)); + color: #334155; + box-shadow: 0 8px 18px rgba(15,23,42,0.05); + } + .stats-pill-label { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 4px 8px; + border-radius: 999px; + font-size: 0.82em; + font-weight: 700; + color: #ffffff; + background: linear-gradient(135deg, #64748b, #475569); + } + .stats-pill-value { + font-weight: 800; + color: #1e293b; + min-width: 20px; + } + .stats-pill-total .stats-pill-label { background: linear-gradient(135deg, #3b82f6, #1d4ed8); } + .stats-pill-people .stats-pill-label { background: linear-gradient(135deg, #0f766e, #14b8a6); } + .stats-pill-text .stats-pill-label { background: linear-gradient(135deg, #8b5cf6, #7c3aed); } + .stats-pill-image .stats-pill-label { background: linear-gradient(135deg, #ec4899, #db2777); } + .stats-pill-video .stats-pill-label { background: linear-gradient(135deg, #f97316, #ea580c); } + .stats-pill-link .stats-pill-label { background: linear-gradient(135deg, #22c55e, #16a34a); } + .stats-pill-emoji .stats-pill-label { background: linear-gradient(135deg, #eab308, #ca8a04); } hr { border: none; height: 1px; background: linear-gradient(90deg, transparent, rgba(148,163,184,0.35), transparent); margin: 26px 0; } a { color: var(--primary); text-decoration: none; border-bottom: 1px dashed rgba(109,94,252,0.35); } .signature { margin-top: 34px; text-align: right; color: #73849c; font-size: 0.95em; font-style: italic; }