From 6fec1025ded3db0eef85c40a8ba23bc20695c9b9 Mon Sep 17 00:00:00 2001 From: liuwei Date: Thu, 23 Apr 2026 10:21:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=895=E8=AF=9D=E9=A2=98=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E9=87=8D=E6=8E=92=E6=80=BB=E7=BB=93=E6=B8=B2=E6=9F=93=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=A8=A1=E6=9D=BF=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增话题卡片聚合逻辑:从结构化分节中提取并合并为最多5个话题\n- 在渲染阶段识别并归并时段/参与人数/核心观点/客观分析/亮点瞬间,避免详情平铺\n- 新增辅助区块抽取(交易/资源/荣誉榜等),以独立模块展示减少正文拥挤\n- 调整Gemini模板为固定5话题卡片布局,控制单话题条目上限,降低超长截图风险\n- 修正统计展示口径兜底:限制Text和Active不超过Msgs,避免出现反直觉指标\n- 保留旧字段兼容,确保非Gemini模板仍可回退渲染 --- plugins/message_summary/main.py | 141 ++++++++++++++++++ .../templates/gemini_summary_card.html | 107 ++++++++++--- 2 files changed, 228 insertions(+), 20 deletions(-) diff --git a/plugins/message_summary/main.py b/plugins/message_summary/main.py index 4f26519..52769bd 100644 --- a/plugins/message_summary/main.py +++ b/plugins/message_summary/main.py @@ -788,6 +788,13 @@ class MessageSummaryPlugin(MessagePluginInterface): link_count = cls._to_int(stats.get("link_count")) emoji_count = cls._to_int(stats.get("emoji_count")) + # 口径兜底: + # 1. 在极端统计异常场景下,文本数/活跃人数不应超过总消息数; + # 2. 这里做展示层修正,避免页面出现“Text > Msgs”这类反直觉数据。 + if total_count > 0: + text_count = min(text_count, total_count) + participant_count = min(participant_count, total_count) + # 媒体量:把图片+视频合并,符合卡片化概览阅读习惯。 media_count = image_count + video_count section_count = len(sections) @@ -885,6 +892,134 @@ class MessageSummaryPlugin(MessagePluginInterface): "latency_text": latency_text, } + @staticmethod + def _clean_topic_title(title: str) -> str: + """清理话题标题噪音,保持模板展示紧凑可读。""" + text = str(title or "").strip() + if not text: + return "未命名话题" + # 去掉 markdown/序号/emoji 前缀噪音。 + text = re.sub(r"^[#\s\d\W_]+", "", text) + text = text.replace("【", "").replace("】", "") + text = re.sub(r"\s+", " ", text).strip() + return text[:42] if text else "未命名话题" + + @classmethod + def _build_topic_cards_from_sections(cls, sections: List[Dict[str, Any]], limit: int = 5) -> List[Dict[str, Any]]: + """把结构化 sections 聚合为“话题卡片”列表(最多 5 个)。""" + if not sections: + return [] + + # 说明: + # 1. LLM 常见输出是“话题标题 + 核心观点/客观分析/亮点瞬间”; + # 2. 这里通过标题关键字与邻近分节聚合,避免渲染成冗长平铺列表。 + topic_start_indices: List[int] = [] + for idx, section in enumerate(sections): + title_text = str(section.get("title") or "").strip() + if not title_text: + continue + if "【" in title_text and "】" in title_text: + topic_start_indices.append(idx) + continue + if re.match(r"^\d+[.)、]\s*", title_text): + topic_start_indices.append(idx) + continue + if any(key in title_text for key in ["话题", "讨论", "专题"]): + topic_start_indices.append(idx) + + if not topic_start_indices: + # 兜底:没有显式话题标题时,按前 N 个分节强制抽取。 + topic_start_indices = list(range(min(limit, len(sections)))) + + # 去重并排序,保证聚合窗口有序。 + topic_start_indices = sorted(set(topic_start_indices))[:limit] + cards: List[Dict[str, Any]] = [] + + for pos, start_idx in enumerate(topic_start_indices): + end_idx = topic_start_indices[pos + 1] if pos + 1 < len(topic_start_indices) else len(sections) + group_sections = sections[start_idx:end_idx] + if not group_sections: + continue + + main_title = cls._clean_topic_title(str(group_sections[0].get("title") or "")) + topic = { + "title": main_title, + "time_range": "", + "participants": "", + "overview_points": [], + "analysis_points": [], + "quote_text": "", + } + + for sec in group_sections: + sec_title = str(sec.get("title") or "").strip() + lower_title = sec_title.lower() + items = sec.get("items") or [] + item_texts = [str(item.get("text") or "").strip() for item in items if str(item.get("text") or "").strip()] + + # 从任意段提取时段/参与人数,兼容“放在 bullet 里”的输出。 + for text in item_texts: + if not topic["time_range"] and ("时段" in text or "时间" in text): + topic["time_range"] = text[:58] + if not topic["participants"] and ("参与人数" in text or "人参与" in text): + topic["participants"] = text[:42] + + # 依据子标题语义归类内容。 + if any(key in lower_title for key in ["核心观点", "观点回顾", "要点"]): + for text in item_texts: + topic["overview_points"].append(text[:120]) + elif any(key in lower_title for key in ["客观分析", "深度分析", "分析"]): + for text in item_texts: + topic["analysis_points"].append(text[:120]) + elif any(key in lower_title for key in ["亮点瞬间", "金句", "高光"]): + for text in item_texts: + if not topic["quote_text"]: + topic["quote_text"] = text[:120] + else: + # 未命中的子分节,优先补进 overview,保持信息不丢。 + for text in item_texts: + topic["overview_points"].append(text[:120]) + + # 控制单卡体积,避免一张卡过长压垮整图布局。 + topic["overview_points"] = topic["overview_points"][:3] + topic["analysis_points"] = topic["analysis_points"][:2] + if not topic["quote_text"] and topic["analysis_points"]: + topic["quote_text"] = topic["analysis_points"][0][:110] + + cards.append(topic) + if len(cards) >= limit: + break + + return cards + + @classmethod + def _build_auxiliary_sections(cls, sections: List[Dict[str, Any]], topic_titles: List[str]) -> List[Dict[str, Any]]: + """抽取非话题区块(如交易/荣誉榜),用于页面底部辅助展示。""" + if not sections: + return [] + title_set = {str(t).strip() for t in topic_titles if str(t).strip()} + aux_sections: List[Dict[str, Any]] = [] + for section in sections: + section_title = cls._clean_topic_title(str(section.get("title") or "")) + if not section_title or section_title in title_set: + continue + # 仅保留明显“功能区”类型标题,防止重复渲染话题细节块。 + if not any(key in section_title for key in ["交易", "资源", "荣誉", "MVP", "排行榜", "总结", "快报"]): + continue + texts: List[str] = [] + for item in section.get("items", []): + text = str(item.get("text") or "").strip() + if text: + texts.append(text[:110]) + if len(texts) >= 4: + break + if not texts: + continue + aux_sections.append({"title": section_title, "items": texts}) + if len(aux_sections) >= 3: + break + return aux_sections + def _render_summary_template_html( self, group_name: str, @@ -903,6 +1038,10 @@ class MessageSummaryPlugin(MessagePluginInterface): layout_data=layout_data, metadata=metadata, ) + sections = layout_data.get("sections", []) or [] + topic_cards = self._build_topic_cards_from_sections(sections, limit=5) + topic_titles = [card.get("title", "") for card in topic_cards] + auxiliary_sections = self._build_auxiliary_sections(sections, topic_titles) # 说明: # 1. 这里注入“本地字体 CSS”到模板,避免依赖 Google Fonts 等外网资源; # 2. 字体文件统一从仓库根目录 fonts/ 下读取,便于部署时统一管理; @@ -918,6 +1057,8 @@ class MessageSummaryPlugin(MessagePluginInterface): "summary_doc_title": layout_data.get("document_title", ""), "summary_lead": layout_data.get("lead", ""), "summary_sections": layout_data.get("sections", []), + "summary_topics": topic_cards, + "summary_aux_sections": auxiliary_sections, "summary_fallback_text": layout_data.get("fallback_text", ""), "summary_metrics": metrics_data, "local_font_css": Markup(local_font_css), diff --git a/plugins/message_summary/templates/gemini_summary_card.html b/plugins/message_summary/templates/gemini_summary_card.html index 4dd2aae..24efe0f 100644 --- a/plugins/message_summary/templates/gemini_summary_card.html +++ b/plugins/message_summary/templates/gemini_summary_card.html @@ -146,30 +146,48 @@ line-height: 1.78; color: #166534; } - .sections { + .topics { margin-top: 12px; display: flex; flex-direction: column; gap: 10px; } - .section-card { + .topic-card { border: 1px solid #e5eaf1; background: #ffffff; border-radius: 8px; padding: 10px 11px; } - .section-title { + .topic-title { margin: 0 0 8px; font-size: 14px; font-weight: 800; color: var(--title); letter-spacing: -.01em; } - .section-items { + .topic-meta { + margin: 0 0 8px; + padding: 8px 9px; + border-radius: 6px; + background: #f8fafc; + border: 1px dashed #dbe4f2; + font-size: 11px; + line-height: 1.65; + color: #5b6b84; + } + .topic-block { display: flex; flex-direction: column; gap: 6px; } + .topic-subtitle { + margin: 0; + font-size: 10px; + text-transform: uppercase; + letter-spacing: .08em; + color: #94a3b8; + font-weight: 800; + } .item-paragraph { margin: 0; font-size: 13px; @@ -256,6 +274,30 @@ gap: 10px; margin-top: 12px; } + .aux-sections { + margin-top: 12px; + display: flex; + flex-direction: column; + gap: 8px; + } + .aux-card { + border: 1px solid #e8edf5; + border-radius: 7px; + padding: 9px 10px; + background: #fff; + } + .aux-title { + margin: 0 0 6px; + font-size: 12px; + font-weight: 800; + color: #334155; + } + .aux-item { + margin: 0; + font-size: 12px; + line-height: 1.68; + color: #475569; + } .mini-card { border: 1px solid #e5eaf1; border-radius: 8px; @@ -357,29 +399,54 @@
{{ summary_lead or "暂无总结内容。" }}
-
- {% for section in summary_sections %} -
-

{{ section["title"] }}

-
- {% for item in section["items"] %} - {% if item["kind"] == "bullet" %} -

{{ item["text"] }}

- {% elif item["kind"] == "quote" %} -
{{ item["text"] }}
- {% elif item["kind"] == "code" %} -
{{ item["text"] }}
- {% else %} -

{{ item["text"] }}

- {% endif %} +
+ {% for topic in summary_topics %} +
+

{{ loop.index }}. {{ topic["title"] }}

+
+ {% if topic["time_range"] %}
🕒 {{ topic["time_range"] }}
{% endif %} + {% if topic["participants"] %}
👥 {{ topic["participants"] }}
{% endif %} +
+ {% if topic["overview_points"] %} +
+

核心观点

+ {% for line in topic["overview_points"] %} +

{{ line }}

{% endfor %}
+ {% endif %} + {% if topic["analysis_points"] %} +
+

客观分析

+ {% for line in topic["analysis_points"] %} +

{{ line }}

+ {% endfor %} +
+ {% endif %} + {% if topic["quote_text"] %} +
+

亮点瞬间

+
{{ topic["quote_text"] }}
+
+ {% endif %}
{% endfor %}
- {% if not summary_sections %} + {% if not summary_topics %}
{{ summary_fallback_text }}
{% endif %} + {% if summary_aux_sections %} +
+ {% for block in summary_aux_sections %} +
+

{{ block["title"] }}

+ {% for line in block["items"] %} +

• {{ line }}

+ {% endfor %} +
+ {% endfor %} +
+ {% endif %}

# Deep Stats