按5话题结构重排总结渲染并优化模板适配

- 新增话题卡片聚合逻辑:从结构化分节中提取并合并为最多5个话题\n- 在渲染阶段识别并归并时段/参与人数/核心观点/客观分析/亮点瞬间,避免详情平铺\n- 新增辅助区块抽取(交易/资源/荣誉榜等),以独立模块展示减少正文拥挤\n- 调整Gemini模板为固定5话题卡片布局,控制单话题条目上限,降低超长截图风险\n- 修正统计展示口径兜底:限制Text和Active不超过Msgs,避免出现反直觉指标\n- 保留旧字段兼容,确保非Gemini模板仍可回退渲染
This commit is contained in:
liuwei
2026-04-23 10:21:00 +08:00
parent 845b58ecc8
commit 6fec1025de
2 changed files with 228 additions and 20 deletions

View File

@@ -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),

View File

@@ -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 @@
<div class="lead-callout">
{{ summary_lead or "暂无总结内容。" }}
</div>
<div class="sections">
{% for section in summary_sections %}
<article class="section-card">
<h3 class="section-title">{{ section["title"] }}</h3>
<div class="section-items">
{% for item in section["items"] %}
{% if item["kind"] == "bullet" %}
<p class="item-bullet">{{ item["text"] }}</p>
{% elif item["kind"] == "quote" %}
<blockquote class="item-quote">{{ item["text"] }}</blockquote>
{% elif item["kind"] == "code" %}
<pre class="item-code">{{ item["text"] }}</pre>
{% else %}
<p class="item-paragraph">{{ item["text"] }}</p>
{% endif %}
<div class="topics">
{% for topic in summary_topics %}
<article class="topic-card">
<h3 class="topic-title">{{ loop.index }}. {{ topic["title"] }}</h3>
<div class="topic-meta">
{% if topic["time_range"] %}<div>🕒 {{ topic["time_range"] }}</div>{% endif %}
{% if topic["participants"] %}<div>👥 {{ topic["participants"] }}</div>{% endif %}
</div>
{% if topic["overview_points"] %}
<div class="topic-block">
<p class="topic-subtitle">核心观点</p>
{% for line in topic["overview_points"] %}
<p class="item-bullet">{{ line }}</p>
{% endfor %}
</div>
{% endif %}
{% if topic["analysis_points"] %}
<div class="topic-block">
<p class="topic-subtitle">客观分析</p>
{% for line in topic["analysis_points"] %}
<p class="item-paragraph">{{ line }}</p>
{% endfor %}
</div>
{% endif %}
{% if topic["quote_text"] %}
<div class="topic-block">
<p class="topic-subtitle">亮点瞬间</p>
<blockquote class="item-quote">{{ topic["quote_text"] }}</blockquote>
</div>
{% endif %}
</article>
{% endfor %}
</div>
{% if not summary_sections %}
{% if not summary_topics %}
<div class="fallback-text">{{ summary_fallback_text }}</div>
{% endif %}
{% if summary_aux_sections %}
<div class="aux-sections">
{% for block in summary_aux_sections %}
<article class="aux-card">
<h4 class="aux-title">{{ block["title"] }}</h4>
{% for line in block["items"] %}
<p class="aux-item">• {{ line }}</p>
{% endfor %}
</article>
{% endfor %}
</div>
{% endif %}
<div class="summary-grid">
<div class="mini-card">
<h4 class="mini-title"># Deep Stats</h4>