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["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 %}