33from __future__ import annotations
44
55import contextlib
6+ from datetime import datetime
67import html
78import io
9+ import json
810import os
911import re
1012import sys
1517
1618from .artifacts import ReportArtifacts , ReportIdentity , market_session
1719from .local_store import load_store
20+ from .research_style import sanitize_public_report
1821
1922
2023class PDFDependencyError (RuntimeError ):
@@ -94,6 +97,63 @@ def _template_text() -> str:
9497 )
9598
9699
100+ def _parse_identity_from_path (path : Path ) -> ReportIdentity | None :
101+ match = re .match (r"^(?P<trade_date>\d{8})-(?P<session>早盘|午间|盘中|盘后)-(?P<topic>.+)\.md$" , path .name )
102+ if not match :
103+ return None
104+ return ReportIdentity (match .group ("trade_date" ), match .group ("session" ), match .group ("topic" ))
105+
106+
107+ def _report_title (markdown : str , trade_date : str ) -> str :
108+ title_match = re .search (r"^#\s+(.+)$" , markdown , flags = re .MULTILINE )
109+ return title_match .group (1 ).strip () if title_match else f"{ trade_date } 投资复盘报告"
110+
111+
112+ def _report_topic (markdown : str , fallback : str = "A股投资日报" ) -> str :
113+ title = re .sub (r"\s+" , "" , _report_title (markdown , "" )).strip ("::-—" )
114+ if title in {"" , "复盘" , "深度复盘" }:
115+ return "A股深度复盘"
116+ if "日报" in title :
117+ return "A股投资日报"
118+ return title or fallback
119+
120+
121+ def _load_related_evidence (markdown_path : Path ) -> dict [str , Any ] | None :
122+ candidates = []
123+ if markdown_path .stem in {"replay" , "report" }:
124+ candidates .append (markdown_path .with_name ("evidence.json" ))
125+ candidates .append (markdown_path .with_name (f"{ markdown_path .stem } -evidence.json" ))
126+ for candidate in candidates :
127+ if not candidate .exists ():
128+ continue
129+ try :
130+ return json .loads (candidate .read_text (encoding = "utf-8" ))
131+ except json .JSONDecodeError :
132+ return None
133+ return None
134+
135+
136+ def _canonicalize_markdown (
137+ artifacts : ReportArtifacts ,
138+ trade_date : str ,
139+ markdown_path : Path ,
140+ markdown : str ,
141+ * ,
142+ now : datetime | None = None ,
143+ ) -> tuple [Path , str ]:
144+ identity = _parse_identity_from_path (markdown_path )
145+ evidence = _load_related_evidence (markdown_path )
146+ cleaned = sanitize_public_report (markdown , evidence )
147+ session = identity .session if identity else market_session (now )
148+ topic = identity .topic if identity else _report_topic (cleaned )
149+ canonical = artifacts .write_report_markdown (ReportIdentity (trade_date , session , topic ), cleaned )
150+ return canonical , cleaned
151+
152+
153+ def _clean_document_html (document : str ) -> str :
154+ return re .sub (r"Kami-compatible editorial layout\s*·?\s*" , "" , document , flags = re .IGNORECASE )
155+
156+
97157def _load_weasyprint () -> Any :
98158 os .environ .setdefault ("XDG_CACHE_HOME" , str (Path (tempfile .gettempdir ()) / "young-stock-weasy-cache" ))
99159 if sys .platform == "darwin" :
@@ -116,9 +176,9 @@ def _default_render(html_path: Path, pdf_path: Path) -> None:
116176 renderer = _load_weasyprint ()
117177 if renderer is None :
118178 raise PDFDependencyError (
119- "未安装 PDF 可选依赖。uv tool 用户请运行 "
120- "`uv tool install --force 'young-stock-cli[pdf]'`; "
121- "普通 Python 环境请运行 `python3 -m pip install \" young-stock-cli[pdf]\" `。"
179+ "当前环境未检测到 PDF 渲染能力。请先运行 `young init` 检查安装状态; "
180+ "若仍缺少依赖,uv tool 用户请执行 `uv tool install --force 'young-stock-cli[pdf]'`, "
181+ "普通 Python 环境请执行 `python3 -m pip install \" young-stock-cli[pdf]\" `。"
122182 )
123183 renderer (filename = str (html_path ), base_url = str (html_path .parent )).write_pdf (str (pdf_path ))
124184
@@ -145,6 +205,7 @@ def export_report_pdf(
145205 profile : dict [str , Any ] | None = None ,
146206 daily_markdown_factory : Callable [[], str ] | None = None ,
147207 render : Callable [[Path , Path ], None ] | None = None ,
208+ now : datetime | None = None ,
148209) -> tuple [Path , Path ]:
149210 artifacts = ReportArtifacts (trade_date )
150211 markdown_path = ReportArtifacts .latest_markdown (trade_date )
@@ -159,13 +220,15 @@ def export_report_pdf(
159220 markdown = _capture_daily (core , trade_date , profile )
160221 else :
161222 raise ValueError ("没有可用报告;请先运行 `young daily` 或提供日报生成器。" )
162- identity = ReportIdentity (trade_date , market_session (), "A股投资日报" )
163- markdown_path = artifacts .write_report_markdown (identity , markdown )
223+ markdown_path = artifacts .write_report_markdown (
224+ ReportIdentity (trade_date , market_session (now ), "A股投资日报" ),
225+ sanitize_public_report (markdown ),
226+ )
164227 markdown = markdown_path .read_text (encoding = "utf-8" )
228+ markdown_path , markdown = _canonicalize_markdown (artifacts , trade_date , markdown_path , markdown , now = now )
165229 body = markdown_to_html (markdown )
166- title_match = re .search (r"^#\s+(.+)$" , markdown , flags = re .MULTILINE )
167- title = title_match .group (1 ).strip () if title_match else f"{ trade_date } 投资复盘报告"
168- document = (
230+ title = _report_title (markdown , trade_date )
231+ document = _clean_document_html (
169232 _template_text ()
170233 .replace ("{{TITLE}}" , html .escape (title ))
171234 .replace ("{{DATE}}" , trade_date )
0 commit comments