马一丁

Fix-Multiple Directories

@@ -59,6 +59,8 @@ class HTMLRenderer: @@ -59,6 +59,8 @@ class HTMLRenderer:
59 self.heading_label_map: Dict[str, Dict[str, Any]] = {} 59 self.heading_label_map: Dict[str, Dict[str, Any]] = {}
60 self.primary_heading_index = 0 60 self.primary_heading_index = 0
61 self.secondary_heading_index = 0 61 self.secondary_heading_index = 0
  62 + self.toc_rendered = False
  63 + self.hero_kpi_signature: tuple | None = None
62 64
63 # ====== 公共入口 ====== 65 # ====== 公共入口 ======
64 66
@@ -78,6 +80,7 @@ class HTMLRenderer: @@ -78,6 +80,7 @@ class HTMLRenderer:
78 self.heading_counter = 0 80 self.heading_counter = 0
79 self.metadata = self.document.get("metadata", {}) or {} 81 self.metadata = self.document.get("metadata", {}) or {}
80 raw_chapters = self.document.get("chapters", []) or [] 82 raw_chapters = self.document.get("chapters", []) or []
  83 + self.toc_rendered = False
81 self.chapters = self._prepare_chapters(raw_chapters) 84 self.chapters = self._prepare_chapters(raw_chapters)
82 self.chapter_anchor_map = { 85 self.chapter_anchor_map = {
83 chapter.get("chapterId"): chapter.get("anchor") 86 chapter.get("chapterId"): chapter.get("anchor")
@@ -90,6 +93,8 @@ class HTMLRenderer: @@ -90,6 +93,8 @@ class HTMLRenderer:
90 metadata = self.metadata 93 metadata = self.metadata
91 theme_tokens = metadata.get("themeTokens") or self.document.get("themeTokens", {}) 94 theme_tokens = metadata.get("themeTokens") or self.document.get("themeTokens", {})
92 title = metadata.get("title") or metadata.get("query") or "智能舆情报告" 95 title = metadata.get("title") or metadata.get("query") or "智能舆情报告"
  96 + hero_kpis = (metadata.get("hero") or {}).get("kpis")
  97 + self.hero_kpi_signature = self._kpi_signature_from_items(hero_kpis)
93 98
94 head = self._render_head(title, theme_tokens) 99 head = self._render_head(title, theme_tokens)
95 body = self._render_body() 100 body = self._render_body()
@@ -320,12 +325,15 @@ class HTMLRenderer: @@ -320,12 +325,15 @@ class HTMLRenderer:
320 """ 325 """
321 if not self.toc_entries: 326 if not self.toc_entries:
322 return "" 327 return ""
  328 + if self.toc_rendered:
  329 + return ""
323 toc_config = self.metadata.get("toc") or {} 330 toc_config = self.metadata.get("toc") or {}
324 toc_title = toc_config.get("title") or "📚 目录" 331 toc_title = toc_config.get("title") or "📚 目录"
325 toc_items = "".join( 332 toc_items = "".join(
326 self._format_toc_entry(entry) 333 self._format_toc_entry(entry)
327 for entry in self.toc_entries 334 for entry in self.toc_entries
328 ) 335 )
  336 + self.toc_rendered = True
329 return f""" 337 return f"""
330 <nav class="toc"> 338 <nav class="toc">
331 <div class="toc-title">{self._escape_html(toc_title)}</div> 339 <div class="toc-title">{self._escape_html(toc_title)}</div>
@@ -965,6 +973,8 @@ class HTMLRenderer: @@ -965,6 +973,8 @@ class HTMLRenderer:
965 973
966 def _render_kpi_grid(self, block: Dict[str, Any]) -> str: 974 def _render_kpi_grid(self, block: Dict[str, Any]) -> str:
967 """渲染KPI卡片栅格,包含指标值与涨跌幅""" 975 """渲染KPI卡片栅格,包含指标值与涨跌幅"""
  976 + if self._should_skip_overview_kpi(block):
  977 + return ""
968 cards = "" 978 cards = ""
969 for item in block.get("items", []): 979 for item in block.get("items", []):
970 delta = item.get("delta") 980 delta = item.get("delta")
@@ -1051,6 +1061,46 @@ class HTMLRenderer: @@ -1051,6 +1061,46 @@ class HTMLRenderer:
1051 """ 1061 """
1052 return table_html 1062 return table_html
1053 1063
  1064 + # ====== Front-matter guards ======
  1065 +
  1066 + def _kpi_signature_from_items(self, items: Any) -> tuple | None:
  1067 + """将KPI数组转换为可比较的签名"""
  1068 + if not isinstance(items, list):
  1069 + return None
  1070 + normalized = []
  1071 + for raw in items:
  1072 + normalized_item = self._normalize_kpi_item(raw)
  1073 + if normalized_item:
  1074 + normalized.append(normalized_item)
  1075 + return tuple(normalized) if normalized else None
  1076 +
  1077 + def _normalize_kpi_item(self, item: Any) -> tuple[str, str, str, str, str] | None:
  1078 + if not isinstance(item, dict):
  1079 + return None
  1080 +
  1081 + def normalize(value: Any) -> str:
  1082 + if value is None:
  1083 + return ""
  1084 + if isinstance(value, (int, float)):
  1085 + return str(value)
  1086 + return str(value).strip()
  1087 +
  1088 + label = normalize(item.get("label"))
  1089 + value = normalize(item.get("value"))
  1090 + unit = normalize(item.get("unit"))
  1091 + delta = normalize(item.get("delta"))
  1092 + tone = normalize(item.get("deltaTone") or item.get("tone"))
  1093 + return label, value, unit, delta, tone
  1094 +
  1095 + def _should_skip_overview_kpi(self, block: Dict[str, Any]) -> bool:
  1096 + """若KPI内容与封面一致,则判定为重复总览"""
  1097 + if not self.hero_kpi_signature:
  1098 + return False
  1099 + block_signature = self._kpi_signature_from_items(block.get("items"))
  1100 + if not block_signature:
  1101 + return False
  1102 + return block_signature == self.hero_kpi_signature
  1103 +
1054 # ====== Inline 渲染 ====== 1104 # ====== Inline 渲染 ======
1055 1105
1056 def _normalize_inline_payload(self, run: Dict[str, Any]) -> tuple[str, List[Dict[str, Any]]]: 1106 def _normalize_inline_payload(self, run: Dict[str, Any]) -> tuple[str, List[Dict[str, Any]]]: