From 845b58ecc89dec7abb7a1f95f567befb8fb20816 Mon Sep 17 00:00:00 2001 From: liuwei Date: Thu, 23 Apr 2026 10:12:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=20Gemini=20=E6=80=BB?= =?UTF-8?q?=E7=BB=93=E6=A8=A1=E6=9D=BF=E8=B5=84=E8=AE=AF=E5=AF=86=E5=BA=A6?= =?UTF-8?q?=E4=B8=8E=E7=BB=9F=E8=AE=A1=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增模板统计视图模型,接入消息总数/活跃人数/文本量/媒体量等核心指标\n- 追加深度统计卡片(Links/Emoji/Video/Sections/Bullets/Quotes/Code)并展示活跃等级\n- 从结构化章节提取话题雷达标签与核心看点,提升信息可读性\n- 调整模板为高信息密度布局,参考 gemini-code 风格进行 KPI、标签与双栏信息区展示\n- 模板模式下停止在正文拼接群概览与 tokens 文本,改由可视化卡片与页脚指标展示\n- 完善渲染链路参数传递:message_stats 与 metadata 全量传入模板渲染 --- plugins/message_summary/main.py | 415 +++++++++++++++++- .../templates/gemini_summary_card.html | 328 +++++++++----- 2 files changed, 627 insertions(+), 116 deletions(-) diff --git a/plugins/message_summary/main.py b/plugins/message_summary/main.py index a73bb52..4f26519 100644 --- a/plugins/message_summary/main.py +++ b/plugins/message_summary/main.py @@ -516,11 +516,393 @@ class MessageSummaryPlugin(MessagePluginInterface): rendered = "".join(html_parts) return cls._sanitize_rendered_html(rendered) - def _render_summary_template_html(self, group_name: str, summary_text: str) -> str: + @staticmethod + def _strip_markdown_inline(text: str) -> str: + """清理行内 Markdown 标记,输出可直接展示的纯文本。 + + 设计说明: + 1. 仅处理常见行内语法(粗体/斜体/代码/链接),避免把模板渲染复杂度继续堆高; + 2. 不在这里做 HTML 生成,保证“结构化渲染层”始终输出纯文本; + 3. 处理后再交给模板做自动转义,避免注入风险。 + """ + cleaned = str(text or "").strip() + if not cleaned: + return "" + # 图片语法:![alt](url) -> alt + cleaned = re.sub(r"!\[([^\]]*)\]\([^)]+\)", r"\1", cleaned) + # 链接语法:[text](url) -> text + cleaned = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", cleaned) + # 行内代码:`code` -> code + cleaned = re.sub(r"`([^`]+)`", r"\1", cleaned) + # 粗体/斜体标记清理 + cleaned = re.sub(r"(\*\*|__)(.*?)\1", r"\2", cleaned) + cleaned = re.sub(r"(\*|_)(.*?)\1", r"\2", cleaned) + # 删除零宽字符,避免昵称或内容出现布局异常。 + cleaned = re.sub(r"[\u200b-\u200f\u2060\ufeff]", "", cleaned) + return cleaned.strip() + + @classmethod + def _extract_llm_payload_text(cls, summary_text: str) -> str: + """从 LLM 返回文本中提取真正的总结正文。 + + 兼容场景: + 1. 直接返回 Markdown 正文; + 2. 返回 JSON 字符串(如 {"text":"..."}); + 3. 返回被双引号包裹且含转义换行的字符串。 + """ + text = str(summary_text or "").strip() + if not text: + return "" + + # 第一层:尝试按 JSON 解析外层包装。 + try: + if (text.startswith("{") and text.endswith("}")) or (text.startswith("[") and text.endswith("]")): + payload = json.loads(text) + if isinstance(payload, dict): + for key in ("text", "summary", "answer", "content", "result"): + value = payload.get(key) + if isinstance(value, str) and value.strip(): + return value.strip() + if isinstance(payload, str) and payload.strip(): + return payload.strip() + except Exception: + pass + + # 第二层:处理被引号包裹的 JSON 字符串(例如 "\"# 标题\\n内容\"")。 + try: + if text.startswith('"') and text.endswith('"'): + decoded = json.loads(text) + if isinstance(decoded, str) and decoded.strip(): + return decoded.strip() + except Exception: + pass + + return text + + @classmethod + def _build_summary_layout_data(cls, summary_text: str) -> Dict[str, Any]: + """把 LLM 总结文本重排为模板可直接消费的结构化数据。 + + 输出结构: + - lead: 导语文本(通常来自一级标题后的首段) + - sections: 分节内容,每节包含 title + items + - fallback_text: 兜底纯文本(解析失败时仍可展示) + """ + raw_text = cls._extract_llm_payload_text(summary_text) + lines = [line.rstrip() for line in str(raw_text or "").splitlines()] + + # 标准化:去除分隔线与空白噪音,减少模板渲染无效块。 + normalized_lines: List[str] = [] + for raw_line in lines: + line = str(raw_line or "").strip() + if not line: + normalized_lines.append("") + continue + if re.fullmatch(r"[-*_]{3,}", line): + # Markdown 横线仅用于语义分隔,结构化渲染里直接跳过。 + continue + normalized_lines.append(line) + + document_title = "" + lead_lines: List[str] = [] + sections: List[Dict[str, Any]] = [] + current_section: Optional[Dict[str, Any]] = None + paragraph_buffer: List[str] = [] + in_code_block = False + code_lines: List[str] = [] + + def flush_paragraph() -> None: + """把段落缓存写入当前分节。""" + nonlocal paragraph_buffer, current_section + if not paragraph_buffer: + return + paragraph_text = cls._strip_markdown_inline(" ".join(paragraph_buffer).strip()) + paragraph_buffer = [] + if not paragraph_text: + return + if current_section is None: + lead_lines.append(paragraph_text) + return + current_section["items"].append({"kind": "paragraph", "text": paragraph_text}) + + def ensure_section(title: str) -> None: + """确保当前分节存在,并切换到新分节。""" + nonlocal current_section + clean_title = cls._strip_markdown_inline(title) or "未命名章节" + current_section = {"title": clean_title, "items": []} + sections.append(current_section) + + for line in normalized_lines: + # 代码块开始/结束处理。 + if line.startswith("```"): + flush_paragraph() + if in_code_block: + if current_section is None: + ensure_section("代码片段") + code_text = "\n".join(code_lines).strip() + if code_text: + current_section["items"].append({"kind": "code", "text": code_text}) + code_lines = [] + in_code_block = False + else: + in_code_block = True + code_lines = [] + continue + + if in_code_block: + code_lines.append(line) + continue + + if not line: + flush_paragraph() + continue + + # 一级标题:用于页面主标题文案,不作为分节。 + h1_match = re.match(r"^#\s+(.+)$", line) + if h1_match: + flush_paragraph() + title_text = cls._strip_markdown_inline(h1_match.group(1)) + if title_text: + document_title = title_text + continue + + # 二级及以下标题统一作为分节标题。 + section_match = re.match(r"^#{2,6}\s+(.+)$", line) + if section_match: + flush_paragraph() + ensure_section(section_match.group(1)) + continue + + # 有些模型会输出“1. 标题”这种无井号标题,识别后转成分节。 + numeric_heading_match = re.match(r"^\d+[.)、]\s+(.+)$", line) + if numeric_heading_match and len(cls._strip_markdown_inline(numeric_heading_match.group(1))) <= 40: + flush_paragraph() + ensure_section(numeric_heading_match.group(1)) + continue + + # 引用块。 + quote_match = re.match(r"^>\s*(.+)$", line) + if quote_match: + flush_paragraph() + if current_section is None: + ensure_section("精选引用") + quote_text = cls._strip_markdown_inline(quote_match.group(1)) + if quote_text: + current_section["items"].append({"kind": "quote", "text": quote_text}) + continue + + # 无序列表。 + bullet_match = re.match(r"^[-*+]\s+(.+)$", line) + if bullet_match: + flush_paragraph() + if current_section is None: + ensure_section("重点清单") + bullet_text = cls._strip_markdown_inline(bullet_match.group(1)) + if bullet_text: + current_section["items"].append({"kind": "bullet", "text": bullet_text}) + continue + + # 有序列表作为 bullet 统一渲染,避免复杂列表嵌套导致样式凌乱。 + ordered_match = re.match(r"^\d+[.)]\s+(.+)$", line) + if ordered_match: + flush_paragraph() + if current_section is None: + ensure_section("重点清单") + ordered_text = cls._strip_markdown_inline(ordered_match.group(1)) + if ordered_text: + current_section["items"].append({"kind": "bullet", "text": ordered_text}) + continue + + # 其余内容按段落累计,遇到空行或结构标记时再落盘。 + paragraph_buffer.append(line) + + flush_paragraph() + if in_code_block and code_lines: + if current_section is None: + ensure_section("代码片段") + code_text = "\n".join(code_lines).strip() + if code_text: + current_section["items"].append({"kind": "code", "text": code_text}) + + # 清理空章节,并过滤“统计注入段”等不需要重复展示的标题。 + skip_titles = {"群概览", "tokens", "统计信息", "消息统计"} + cleaned_sections: List[Dict[str, Any]] = [] + for section in sections: + if not section.get("items"): + continue + title_text = str(section.get("title") or "").strip().lower() + if title_text in skip_titles: + continue + cleaned_sections.append(section) + + # 导语优先使用 lead_lines,否则取首个章节首条,保证顶部一定有可读摘要。 + lead = " ".join(lead_lines).strip() + if not lead and cleaned_sections: + first_items = cleaned_sections[0].get("items") or [] + if first_items: + lead = str(first_items[0].get("text", "")).strip() + if not lead: + lead = "暂无总结内容。" + + # 模板兜底纯文本:用于极端解析失败场景。 + fallback_text = cls._strip_markdown_inline( + "\n".join([line for line in normalized_lines if line]).strip() + ) or "暂无总结内容。" + + return { + "document_title": document_title, + "lead": lead, + "sections": cleaned_sections, + "fallback_text": fallback_text, + } + + @staticmethod + def _to_int(value: Any) -> int: + """安全转换为整数,避免模板渲染阶段因脏数据抛错。""" + try: + return int(value or 0) + except Exception: + return 0 + + @classmethod + def _build_summary_template_metrics( + cls, + message_stats: Optional[Dict[str, Any]], + layout_data: Dict[str, Any], + metadata: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + """构建 Gemini 模板高信息密度展示所需的统计视图数据。""" + # 说明: + # 1. 该函数专注“模板展示层”数据准备,不改动业务存储; + # 2. 尽量从 message_stats 与结构化 sections 中提取可读指标; + # 3. 数据不全时采用稳妥兜底,避免模板渲染失败。 + stats = message_stats or {} + usage = (metadata or {}).get("usage", {}) or {} + sections = layout_data.get("sections") or [] + + total_count = cls._to_int(stats.get("total_count")) + participant_count = cls._to_int(stats.get("participant_count")) + text_count = cls._to_int(stats.get("text_count")) + image_count = cls._to_int(stats.get("image_count")) + video_count = cls._to_int(stats.get("video_count")) + link_count = cls._to_int(stats.get("link_count")) + emoji_count = cls._to_int(stats.get("emoji_count")) + + # 媒体量:把图片+视频合并,符合卡片化概览阅读习惯。 + media_count = image_count + video_count + section_count = len(sections) + bullet_count = 0 + quote_count = 0 + code_count = 0 + for section in sections: + for item in section.get("items", []): + kind = str(item.get("kind") or "") + if kind == "bullet": + bullet_count += 1 + elif kind == "quote": + quote_count += 1 + elif kind == "code": + code_count += 1 + + # 活跃度等级:仅用于 UI 文案提示,不参与核心业务计算。 + if total_count >= 1600: + activity_badge = "爆炸活跃" + elif total_count >= 1000: + activity_badge = "高活跃" + elif total_count >= 400: + activity_badge = "中活跃" + else: + activity_badge = "常规活跃" + + # 话题雷达:优先取前几节标题,避免依赖额外 LLM 输出字段。 + topic_tags: List[str] = [] + for section in sections: + title_text = str(section.get("title") or "").strip() + if not title_text: + continue + normalized = re.sub(r"^[\d\W_]+", "", title_text) + normalized = re.sub(r"\s+", " ", normalized).strip() + if not normalized: + continue + topic_tags.append(normalized[:24]) + if len(topic_tags) >= 6: + break + + # 核心看点:优先抓 bullet,没有则回退 paragraph。 + highlights: List[str] = [] + for section in sections: + for item in section.get("items", []): + item_text = str(item.get("text") or "").strip() + if not item_text: + continue + if item.get("kind") == "bullet": + highlights.append(item_text[:120]) + if len(highlights) >= 4: + break + if len(highlights) >= 4: + break + if not highlights: + for section in sections: + for item in section.get("items", []): + item_text = str(item.get("text") or "").strip() + if not item_text: + continue + highlights.append(item_text[:120]) + if len(highlights) >= 4: + break + if len(highlights) >= 4: + break + + # 使用统计:读取模型调用元数据,辅助观测总结成本。 + total_tokens = cls._to_int(usage.get("total_tokens")) + prompt_tokens = cls._to_int(usage.get("prompt_tokens")) + completion_tokens = cls._to_int(usage.get("completion_tokens")) + latency = usage.get("latency") + latency_text = f"{latency}s" if latency not in (None, "") else "-" + + return { + "kpi_cards": [ + {"label": "Msgs", "value": total_count, "tone": "slate"}, + {"label": "Active", "value": participant_count, "tone": "blue"}, + {"label": "Text", "value": text_count, "tone": "violet"}, + {"label": "Media", "value": media_count, "tone": "emerald"}, + ], + "mini_stats": [ + {"label": "Links", "value": link_count}, + {"label": "Emoji", "value": emoji_count}, + {"label": "Video", "value": video_count}, + {"label": "Sections", "value": section_count}, + {"label": "Bullets", "value": bullet_count}, + {"label": "Quotes", "value": quote_count}, + {"label": "Code", "value": code_count}, + ], + "topic_tags": topic_tags, + "highlights": highlights, + "activity_badge": activity_badge, + "token_total": total_tokens, + "token_prompt": prompt_tokens, + "token_completion": completion_tokens, + "latency_text": latency_text, + } + + def _render_summary_template_html( + self, + group_name: str, + summary_text: str, + message_stats: Optional[Dict[str, Any]] = None, + metadata: Optional[Dict[str, Any]] = None, + ) -> str: """根据模板路径渲染总结图片 HTML。""" - # 约束:模板只负责展示,正文仍然由模型生成并在此做安全转义后注入。 + # 约束: + # 1. 不再把 LLM 原文直接转 HTML 内嵌到模板; + # 2. 先结构化解析文本,再由模板按组件渲染,稳定控制最终排版。 renderer = HtmlTemplateRenderer() - summary_html = self._summary_markdown_to_html(summary_text) + layout_data = self._build_summary_layout_data(summary_text) + metrics_data = self._build_summary_template_metrics( + message_stats=message_stats, + layout_data=layout_data, + metadata=metadata, + ) # 说明: # 1. 这里注入“本地字体 CSS”到模板,避免依赖 Google Fonts 等外网资源; # 2. 字体文件统一从仓库根目录 fonts/ 下读取,便于部署时统一管理; @@ -531,7 +913,13 @@ class MessageSummaryPlugin(MessagePluginInterface): { "title": f"{group_name} 群聊总结", "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "summary_html": Markup(summary_html), + # 兼容字段:保留给旧模板使用,新版 Gemini 模板已不再依赖该字段。 + "summary_html": Markup(self._summary_markdown_to_html(summary_text)), + "summary_doc_title": layout_data.get("document_title", ""), + "summary_lead": layout_data.get("lead", ""), + "summary_sections": layout_data.get("sections", []), + "summary_fallback_text": layout_data.get("fallback_text", ""), + "summary_metrics": metrics_data, "local_font_css": Markup(local_font_css), }, ) @@ -631,6 +1019,8 @@ class MessageSummaryPlugin(MessagePluginInterface): self, answer: str, group_name: str, + message_stats: Optional[Dict[str, Any]], + metadata: Optional[Dict[str, Any]], output_filename: str, total_timeout: int, ) -> str: @@ -642,7 +1032,12 @@ class MessageSummaryPlugin(MessagePluginInterface): # 优先模板渲染,便于后续仅改 HTML 文件完成样式迭代。 if self._summary_image_mode != "markdown": try: - html_content = self._render_summary_template_html(group_name=group_name, summary_text=answer) + html_content = self._render_summary_template_html( + group_name=group_name, + summary_text=answer, + message_stats=message_stats, + metadata=metadata, + ) await asyncio.wait_for(html_to_image(html_content, str(output_path)), timeout=total_timeout) if not output_path.exists() or output_path.stat().st_size < 1024: raise RuntimeError("模板截图输出文件异常") @@ -800,8 +1195,12 @@ class MessageSummaryPlugin(MessagePluginInterface): answer = self._clean_summary_output(response.get("text", "")) metadata = {"usage": response.get("usage", {}) or {}} spath = "" - answer = self._prepend_stats_section(answer, message_stats or {}) - answer = self._append_usage_info(answer, metadata) + # 说明: + # 1. 模板模式下统计信息由卡片呈现,不再额外拼接“群概览”文本块; + # 2. markdown 模式仍保留旧逻辑,保证兼容历史输出。 + if self._summary_image_mode == "markdown": + answer = self._prepend_stats_section(answer, message_stats or {}) + answer = self._append_usage_info(answer, metadata) if answer and len(answer.strip()) > 0: try: @@ -814,6 +1213,8 @@ class MessageSummaryPlugin(MessagePluginInterface): spath = await self._render_summary_image( answer=answer, group_name=group_name, + message_stats=message_stats or {}, + metadata=metadata, output_filename=output_path, total_timeout=total_timeout, ) diff --git a/plugins/message_summary/templates/gemini_summary_card.html b/plugins/message_summary/templates/gemini_summary_card.html index 617401d..4dd2aae 100644 --- a/plugins/message_summary/templates/gemini_summary_card.html +++ b/plugins/message_summary/templates/gemini_summary_card.html @@ -115,118 +115,106 @@ .meta-value.brand { color: var(--brand); } + .topic-radar { + margin-top: 12px; + } + .topic-tags { + margin-top: 6px; + display: flex; + flex-wrap: wrap; + gap: 6px; + } + .topic-tag { + padding: 3px 8px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + color: #334155; + background: #f8fafc; + border: 1px solid #e2e8f0; + } .summary-body { padding: 18px 20px 20px; } - .markdown-body { + .lead-callout { + margin-top: 10px; + padding: 10px 12px; + background: var(--ok-soft); + border-left: 2px solid var(--ok-line); + border-radius: 0 6px 6px 0; font-size: 13px; - line-height: 1.86; - color: var(--text); - word-wrap: break-word; + line-height: 1.78; + color: #166534; } - .markdown-body > *:first-child { margin-top: 0; } - .markdown-body > *:last-child { margin-bottom: 0; } - .markdown-body h1, - .markdown-body h2, - .markdown-body h3, - .markdown-body h4, - .markdown-body h5, - .markdown-body h6 { - color: var(--title); - margin: 16px 0 8px; - line-height: 1.42; + .sections { + margin-top: 12px; + display: flex; + flex-direction: column; + gap: 10px; + } + .section-card { + border: 1px solid #e5eaf1; + background: #ffffff; + border-radius: 8px; + padding: 10px 11px; + } + .section-title { + margin: 0 0 8px; + font-size: 14px; font-weight: 800; + color: var(--title); letter-spacing: -.01em; } - .markdown-body h1 { font-size: 20px; border-bottom: 1px solid var(--line); padding-bottom: 6px; } - .markdown-body h2 { font-size: 18px; } - .markdown-body h3 { font-size: 16px; } - .markdown-body h4, - .markdown-body h5, - .markdown-body h6 { font-size: 14px; } - .markdown-body p { margin: 10px 0; } - .markdown-body ul, - .markdown-body ol { - margin: 8px 0 12px; - padding-left: 18px; + .section-items { + display: flex; + flex-direction: column; + gap: 6px; } - .markdown-body li { margin: 4px 0; } - .markdown-body blockquote { - margin: 10px 0; + .item-paragraph { + margin: 0; + font-size: 13px; + line-height: 1.82; + color: var(--text); + } + .item-bullet { + margin: 0; + padding-left: 14px; + position: relative; + font-size: 13px; + line-height: 1.78; + color: var(--text); + } + .item-bullet::before { + content: "•"; + position: absolute; + left: 0; + top: 0; + color: var(--brand); + font-weight: 900; + } + .item-quote { + margin: 0; padding: 8px 10px; border-left: 3px solid var(--quote-line); background: var(--quote-bg); color: #475569; border-radius: 0 4px 4px 0; - } - .markdown-body hr { - border: none; - border-top: 1px dashed #dbe3ee; - margin: 14px 0; - } - .markdown-body a { - color: var(--brand); - text-decoration: none; - border-bottom: 1px dotted rgba(37, 99, 235, .5); - } - .markdown-body strong { - color: #1e293b; - font-weight: 800; - } - .markdown-body em { - color: #475569; - font-style: italic; - } - .markdown-body code { - /* 说明:代码字体优先使用本地/系统等宽字体栈,保证服务端离线场景可读。 */ - font-family: var(--abot-font-code, "Cascadia Mono", "Consolas", "SFMono-Regular", Menlo, monospace); font-size: 12px; - background: #eff6ff; - color: #1e40af; - padding: 1px 4px; - border-radius: 4px; + line-height: 1.75; } - .markdown-body pre { - margin: 10px 0; + .item-code { + margin: 0; padding: 10px 11px; border-radius: 6px; background: var(--code-bg); color: var(--code-text); - overflow-x: auto; border: 1px solid #1e293b; - } - .markdown-body pre code { - background: transparent; - color: inherit; - padding: 0; - border-radius: 0; + /* 说明:代码字体优先使用本地/系统等宽字体栈,保证服务端离线场景可读。 */ + font-family: var(--abot-font-code, "Cascadia Mono", "Consolas", "SFMono-Regular", Menlo, monospace); font-size: 12px; line-height: 1.7; - } - .markdown-body table { - width: 100%; - border-collapse: collapse; - margin: 10px 0; - font-size: 11px; - border: 1px solid #e2e8f0; - } - .markdown-body th, - .markdown-body td { - border: 1px solid #e2e8f0; - padding: 6px 7px; - text-align: left; - vertical-align: top; - } - .markdown-body th { - background: #f8fafc; - color: #334155; - font-weight: 700; - } - .markdown-body img { - max-width: 100%; - border-radius: 6px; - border: 1px solid #e2e8f0; - margin: 6px 0; + white-space: pre-wrap; + word-break: break-word; } .summary-footer { margin-top: 14px; @@ -255,6 +243,82 @@ font-size: 12px; color: #166534; } + .fallback-text { + margin-top: 10px; + font-size: 12px; + line-height: 1.76; + color: #475569; + white-space: pre-wrap; + } + .summary-grid { + display: grid; + grid-template-columns: 1.45fr 1fr; + gap: 10px; + margin-top: 12px; + } + .mini-card { + border: 1px solid #e5eaf1; + border-radius: 8px; + padding: 10px 11px; + background: #fff; + } + .mini-title { + margin: 0 0 8px; + font-size: 10px; + text-transform: uppercase; + letter-spacing: .08em; + color: #94a3b8; + font-weight: 800; + } + .mini-list { + display: flex; + flex-direction: column; + gap: 5px; + } + .mini-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + font-size: 12px; + color: #475569; + } + .mini-row .value { + font-weight: 800; + color: #1e293b; + font-family: var(--abot-font-code, "Cascadia Mono", "Consolas", monospace); + } + .highlight-list { + display: flex; + flex-direction: column; + gap: 6px; + } + .highlight-item { + margin: 0; + font-size: 12px; + line-height: 1.72; + color: #334155; + padding-left: 12px; + position: relative; + } + .highlight-item::before { + content: ""; + position: absolute; + left: 0; + top: 7px; + width: 5px; + height: 5px; + border-radius: 50%; + background: #2563eb; + } + @media (max-width: 640px) { + .summary-grid { + grid-template-columns: 1fr; + } + .meta-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } @@ -265,39 +329,85 @@
CHAT INSIGHTS SUMMARY
{{ generated_at }}
-
Daily Archive
+
{{ summary_metrics.activity_badge or "Daily Archive" }}
+ {% for card in summary_metrics.kpi_cards %}
- Type -
群总结
+ {{ card.label }} +
{{ card.value }}
-
- Mode -
Template
-
-
- Render -
HTML
-
-
- Engine -
LLM
+ {% endfor %} +
+
+ # Personal Interest Radar +
+ {% for tag in summary_metrics.topic_tags %} + {{ tag }} + {% endfor %} + {% if not summary_metrics.topic_tags %} + 暂无热点标签 + {% endif %}
- # {{ title }} -
- {{ summary_html }} + # {{ summary_doc_title or title }} +
+ {{ summary_lead or "暂无总结内容。" }}
-
- 内容已按 Markdown 富标签样式渲染(标题、列表、表格、代码块、引用)。 +
+ {% 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 %} + {% endfor %} +
+
+ {% endfor %}
+ {% if not summary_sections %} +
{{ summary_fallback_text }}
+ {% endif %} +
+
+

# Deep Stats

+
+ {% for item in summary_metrics.mini_stats %} +
+ {{ item.label }} + {{ item.value }} +
+ {% endfor %} +
+
+
+

# Core Highlights

+
+ {% for text in summary_metrics.highlights %} +

{{ text }}

+ {% endfor %} + {% if not summary_metrics.highlights %} +

暂无可提取的核心看点

+ {% endif %} +
+
+
+
内容已按结构化规则重排渲染,不再直接内嵌原始 HTML。