马一丁

Add Comments

@@ -64,6 +64,7 @@ class ChapterContentError(ValueError): @@ -64,6 +64,7 @@ class ChapterContentError(ValueError):
64 narrative_characters: int = 0, 64 narrative_characters: int = 0,
65 non_heading_blocks: int = 0, 65 non_heading_blocks: int = 0,
66 ): 66 ):
  67 + """保存本次异常的正文特征,供重试与兜底策略参考。"""
67 super().__init__(message) 68 super().__init__(message)
68 self.chapter_payload: Optional[Dict[str, Any]] = chapter 69 self.chapter_payload: Optional[Dict[str, Any]] = chapter
69 self.body_characters: int = int(body_characters or 0) 70 self.body_characters: int = int(body_characters or 0)
@@ -1018,6 +1019,7 @@ class ChapterGenerationNode(BaseNode): @@ -1018,6 +1019,7 @@ class ChapterGenerationNode(BaseNode):
1018 """ 1019 """
1019 1020
1020 def walk(node: Any) -> int: 1021 def walk(node: Any) -> int:
  1022 + """递归遍历叙述性节点,忽略图表/目录等非正文结构"""
1021 if node is None: 1023 if node is None:
1022 return 0 1024 return 0
1023 if isinstance(node, list): 1025 if isinstance(node, list):
@@ -797,6 +797,7 @@ class ChartToSVGConverter: @@ -797,6 +797,7 @@ class ChartToSVGConverter:
797 colors = self._get_colors(datasets) 797 colors = self._get_colors(datasets)
798 798
799 def _safe_radius(raw) -> float: 799 def _safe_radius(raw) -> float:
  800 + """将输入半径安全转为浮点并设置最小阈值,避免气泡完全消失"""
800 try: 801 try:
801 val = float(raw) 802 val = float(raw)
802 return max(val, 0.5) 803 return max(val, 0.5)
@@ -1764,6 +1764,7 @@ class HTMLRenderer: @@ -1764,6 +1764,7 @@ class HTMLRenderer:
1764 ) -> str: 1764 ) -> str:
1765 """为词云提供表格兜底,避免WordCloud渲染失败后页面空白""" 1765 """为词云提供表格兜底,避免WordCloud渲染失败后页面空白"""
1766 def _collect_items(raw: Any) -> list[dict]: 1766 def _collect_items(raw: Any) -> list[dict]:
  1767 + """将多种词云输入格式(数组/对象/元组/纯文本)规整为统一的词条列表"""
1767 collected: list[dict] = [] 1768 collected: list[dict] = []
1768 if isinstance(raw, list): 1769 if isinstance(raw, list):
1769 for item in raw: 1770 for item in raw:
@@ -1812,6 +1813,7 @@ class HTMLRenderer: @@ -1812,6 +1813,7 @@ class HTMLRenderer:
1812 return "" 1813 return ""
1813 1814
1814 def _format_weight(value: Any) -> str: 1815 def _format_weight(value: Any) -> str:
  1816 + """统一格式化权重,支持百分比/数值与字符串回退"""
1815 if isinstance(value, (int, float)) and not isinstance(value, bool): 1817 if isinstance(value, (int, float)) and not isinstance(value, bool):
1816 if 0 <= value <= 1.5: 1818 if 0 <= value <= 1.5:
1817 return f"{value * 100:.1f}%" 1819 return f"{value * 100:.1f}%"
@@ -667,6 +667,7 @@ class PDFRenderer: @@ -667,6 +667,7 @@ class PDFRenderer:
667 fallback_pattern = rf'<div class="chart-fallback"([^>]*data-widget-id="{re.escape(widget_id)}"[^>]*)>' 667 fallback_pattern = rf'<div class="chart-fallback"([^>]*data-widget-id="{re.escape(widget_id)}"[^>]*)>'
668 668
669 def _hide_fallback(m: re.Match) -> str: 669 def _hide_fallback(m: re.Match) -> str:
  670 + """为匹配到的图表fallback添加隐藏类,防止PDF中重复渲染"""
670 tag = m.group(0) 671 tag = m.group(0)
671 if 'svg-hidden' in tag: 672 if 'svg-hidden' in tag:
672 return tag 673 return tag
@@ -712,6 +713,7 @@ class PDFRenderer: @@ -712,6 +713,7 @@ class PDFRenderer:
712 fallback_pattern = rf'<div class="chart-fallback"([^>]*data-widget-id="{re.escape(widget_id)}"[^>]*)>' 713 fallback_pattern = rf'<div class="chart-fallback"([^>]*data-widget-id="{re.escape(widget_id)}"[^>]*)>'
713 714
714 def _hide_fallback(m: re.Match) -> str: 715 def _hide_fallback(m: re.Match) -> str:
  716 + """匹配词云表格兜底并打上隐藏标记,避免SVG/图片重复显示"""
715 tag = m.group(0) 717 tag = m.group(0)
716 if 'svg-hidden' in tag: 718 if 'svg-hidden' in tag:
717 return tag 719 return tag
@@ -87,7 +87,7 @@ class ChartValidator: @@ -87,7 +87,7 @@ class ChartValidator:
87 } 87 }
88 88
89 def __init__(self): 89 def __init__(self):
90 - pass 90 + """初始化验证器并预留缓存结构,便于后续复用验证/修复结果"""
91 91
92 def validate(self, widget_block: Dict[str, Any]) -> ValidationResult: 92 def validate(self, widget_block: Dict[str, Any]) -> ValidationResult:
93 """ 93 """
@@ -136,6 +136,7 @@ class ChartValidator: @@ -136,6 +136,7 @@ class ChartValidator:
136 136
137 # 检测是否使用了{x, y}形式的数据点(通常用于时间轴/散点) 137 # 检测是否使用了{x, y}形式的数据点(通常用于时间轴/散点)
138 def contains_object_points(ds_list: List[Any] | None) -> bool: 138 def contains_object_points(ds_list: List[Any] | None) -> bool:
  139 + """检查数据集中是否包含以x/y键表示的对象点,用于切换验证分支"""
139 if not isinstance(ds_list, list): 140 if not isinstance(ds_list, list):
140 return False 141 return False
141 for point in ds_list: 142 for point in ds_list:
@@ -432,6 +433,7 @@ class ChartRepairer: @@ -432,6 +433,7 @@ class ChartRepairer:
432 return copy.deepcopy(cached) 433 return copy.deepcopy(cached)
433 434
434 def _cache_and_return(res: RepairResult) -> RepairResult: 435 def _cache_and_return(res: RepairResult) -> RepairResult:
  436 + """写入修复结果缓存并返回,避免重复调用下游修复逻辑"""
435 try: 437 try:
436 self._result_cache[cache_key] = copy.deepcopy(res) 438 self._result_cache[cache_key] = copy.deepcopy(res)
437 except Exception: 439 except Exception:
@@ -27,6 +27,7 @@ def _get_platform_specific_instructions(): @@ -27,6 +27,7 @@ def _get_platform_specific_instructions():
27 system = platform.system() 27 system = platform.system()
28 28
29 def _box_lines(lines): 29 def _box_lines(lines):
  30 + """批量将多行文本包装成带边框的提示块"""
30 return "".join(_box_line(line) for line in lines) 31 return "".join(_box_line(line) for line in lines)
31 32
32 if system == "Darwin": # macOS 33 if system == "Darwin": # macOS
@@ -107,6 +108,7 @@ def _ensure_windows_gtk_paths(): @@ -107,6 +108,7 @@ def _ensure_windows_gtk_paths():
107 seen = set() 108 seen = set()
108 109
109 def _add_candidate(path_like): 110 def _add_candidate(path_like):
  111 + """收集可能的GTK安装路径,避免重复并兼容用户自定义目录"""
110 if not path_like: 112 if not path_like:
111 return 113 return
112 p = Path(path_like) 114 p = Path(path_like)
@@ -18,7 +18,19 @@ from ReportEngine.utils.config import settings @@ -18,7 +18,19 @@ from ReportEngine.utils.config import settings
18 18
19 19
20 def find_latest_run_dir(chapter_root: Path): 20 def find_latest_run_dir(chapter_root: Path):
21 - """定位包含 manifest.json 的最新章节输出目录。""" 21 + """
  22 + 定位章节根目录下最新一次运行的输出目录。
  23 +
  24 + 扫描 `chapter_root` 下所有子目录,筛选出包含 `manifest.json`
  25 + 的候选,按修改时间倒序取最新一条。若目录不存在或没有有效
  26 + manifest,会记录错误并返回 None。
  27 +
  28 + 参数:
  29 + chapter_root: 章节输出的根目录(通常是 settings.CHAPTER_OUTPUT_DIR)
  30 +
  31 + 返回:
  32 + Path | None: 最新的 run 目录路径;若未找到则为 None。
  33 + """
22 if not chapter_root.exists(): 34 if not chapter_root.exists():
23 logger.error(f"章节目录不存在: {chapter_root}") 35 logger.error(f"章节目录不存在: {chapter_root}")
24 return None 36 return None
@@ -41,7 +53,18 @@ def find_latest_run_dir(chapter_root: Path): @@ -41,7 +53,18 @@ def find_latest_run_dir(chapter_root: Path):
41 53
42 54
43 def load_manifest(run_dir: Path): 55 def load_manifest(run_dir: Path):
44 - """读取manifest.json并返回report_id与metadata。""" 56 + """
  57 + 读取单次运行目录内的 manifest.json。
  58 +
  59 + 成功时返回 reportId 以及元数据字典;读取或解析失败会记录错误
  60 + 并返回 (None, None),以便上层提前终止流程。
  61 +
  62 + 参数:
  63 + run_dir: 包含 manifest.json 的章节输出目录
  64 +
  65 + 返回:
  66 + tuple[str | None, dict | None]: (report_id, metadata)
  67 + """
45 manifest_path = run_dir / "manifest.json" 68 manifest_path = run_dir / "manifest.json"
46 try: 69 try:
47 with manifest_path.open("r", encoding="utf-8") as f: 70 with manifest_path.open("r", encoding="utf-8") as f:
@@ -58,7 +81,18 @@ def load_manifest(run_dir: Path): @@ -58,7 +81,18 @@ def load_manifest(run_dir: Path):
58 81
59 82
60 def load_chapters(run_dir: Path): 83 def load_chapters(run_dir: Path):
61 - """加载章节JSON列表。""" 84 + """
  85 + 读取指定 run 目录下的所有章节 JSON。
  86 +
  87 + 会复用 ChapterStorage 的 load_chapters 能力,自动按 order 排序。
  88 + 读取后打印章节数量,便于确认完整性。
  89 +
  90 + 参数:
  91 + run_dir: 单次报告的章节目录
  92 +
  93 + 返回:
  94 + list[dict]: 章节 JSON 列表(若目录为空则为空列表)
  95 + """
62 storage = ChapterStorage(settings.CHAPTER_OUTPUT_DIR) 96 storage = ChapterStorage(settings.CHAPTER_OUTPUT_DIR)
63 chapters = storage.load_chapters(run_dir) 97 chapters = storage.load_chapters(run_dir)
64 logger.info(f"加载章节数: {len(chapters)}") 98 logger.info(f"加载章节数: {len(chapters)}")
@@ -66,7 +100,15 @@ def load_chapters(run_dir: Path): @@ -66,7 +100,15 @@ def load_chapters(run_dir: Path):
66 100
67 101
68 def validate_chapters(chapters): 102 def validate_chapters(chapters):
69 - """使用IRValidator做快速校验,仅记录警告不阻断流程。""" 103 + """
  104 + 使用 IRValidator 对章节结构做快速校验。
  105 +
  106 + 仅记录未通过的章节及前三条错误,不会中断流程;目的是在
  107 + 重装订前发现潜在结构问题。
  108 +
  109 + 参数:
  110 + chapters: 章节 JSON 列表
  111 + """
70 validator = IRValidator() 112 validator = IRValidator()
71 invalid = [] 113 invalid = []
72 for chapter in chapters: 114 for chapter in chapters:
@@ -84,7 +126,20 @@ def validate_chapters(chapters): @@ -84,7 +126,20 @@ def validate_chapters(chapters):
84 126
85 127
86 def stitch_document(report_id, metadata, chapters): 128 def stitch_document(report_id, metadata, chapters):
87 - """将章节装订为整本Document IR。""" 129 + """
  130 + 将各章节与元数据装订为完整的 Document IR。
  131 +
  132 + 使用 DocumentComposer 统一处理章节顺序、全局元数据等,并打印
  133 + 装订完成的章节与图表数量。
  134 +
  135 + 参数:
  136 + report_id: 报告 ID(来自 manifest 或目录名)
  137 + metadata: manifest 中的全局元数据
  138 + chapters: 已加载的章节列表
  139 +
  140 + 返回:
  141 + dict: 完整的 Document IR 对象
  142 + """
88 composer = DocumentComposer() 143 composer = DocumentComposer()
89 document_ir = composer.build_document(report_id, metadata, chapters) 144 document_ir = composer.build_document(report_id, metadata, chapters)
90 logger.info( 145 logger.info(
@@ -95,7 +150,18 @@ def stitch_document(report_id, metadata, chapters): @@ -95,7 +150,18 @@ def stitch_document(report_id, metadata, chapters):
95 150
96 151
97 def count_charts(document_ir): 152 def count_charts(document_ir):
98 - """统计IR中的图表数量。""" 153 + """
  154 + 统计整本 Document IR 中的 Chart.js 图表数量。
  155 +
  156 + 会遍历每章的 blocks,递归查找 widget 类型中以 `chart.js`
  157 + 开头的组件,便于快速感知图表规模。
  158 +
  159 + 参数:
  160 + document_ir: 完整的 Document IR
  161 +
  162 + 返回:
  163 + int: 图表总数
  164 + """
99 chart_count = 0 165 chart_count = 0
100 for chapter in document_ir.get("chapters", []): 166 for chapter in document_ir.get("chapters", []):
101 blocks = chapter.get("blocks", []) 167 blocks = chapter.get("blocks", [])
@@ -104,7 +170,17 @@ def count_charts(document_ir): @@ -104,7 +170,17 @@ def count_charts(document_ir):
104 170
105 171
106 def _count_chart_blocks(blocks): 172 def _count_chart_blocks(blocks):
107 - """递归统计chart.js组件。""" 173 + """
  174 + 递归统计 block 列表中的 Chart.js 组件数量。
  175 +
  176 + 兼容嵌套的 blocks/list/table 结构,确保所有层级的图表都被计入。
  177 +
  178 + 参数:
  179 + blocks: 任意层级的 block 列表
  180 +
  181 + 返回:
  182 + int: 统计到的 chart.js 图表数量
  183 + """
108 count = 0 184 count = 0
109 for block in blocks: 185 for block in blocks:
110 if not isinstance(block, dict): 186 if not isinstance(block, dict):
@@ -129,7 +205,20 @@ def _count_chart_blocks(blocks): @@ -129,7 +205,20 @@ def _count_chart_blocks(blocks):
129 205
130 206
131 def save_document_ir(document_ir, base_name, timestamp): 207 def save_document_ir(document_ir, base_name, timestamp):
132 - """将装订好的IR重新落盘,便于后续复用。""" 208 + """
  209 + 将重新装订好的整本 Document IR 落盘。
  210 +
  211 + 按 `report_ir_{slug}_{timestamp}_regen.json` 命名写入
  212 + `settings.DOCUMENT_IR_OUTPUT_DIR`,确保目录存在并返回保存路径。
  213 +
  214 + 参数:
  215 + document_ir: 已装订完成的整本 IR
  216 + base_name: 由主题/标题生成的安全文件名片段
  217 + timestamp: 时间戳字符串,用于区分多次重生成
  218 +
  219 + 返回:
  220 + Path: 保存的 IR 文件路径
  221 + """
133 output_dir = Path(settings.DOCUMENT_IR_OUTPUT_DIR) 222 output_dir = Path(settings.DOCUMENT_IR_OUTPUT_DIR)
134 output_dir.mkdir(parents=True, exist_ok=True) 223 output_dir.mkdir(parents=True, exist_ok=True)
135 ir_filename = f"report_ir_{base_name}_{timestamp}_regen.json" 224 ir_filename = f"report_ir_{base_name}_{timestamp}_regen.json"
@@ -140,7 +229,20 @@ def save_document_ir(document_ir, base_name, timestamp): @@ -140,7 +229,20 @@ def save_document_ir(document_ir, base_name, timestamp):
140 229
141 230
142 def render_html(document_ir, base_name, timestamp): 231 def render_html(document_ir, base_name, timestamp):
143 - """使用HTMLRenderer渲染并落盘HTML文件。""" 232 + """
  233 + 使用 HTMLRenderer 将 Document IR 渲染为 HTML 并保存。
  234 +
  235 + 渲染后落盘到 `final_reports/html`,打印图表验证统计信息,方便
  236 + 观察 Chart.js 数据的修复/失败情况。
  237 +
  238 + 参数:
  239 + document_ir: 装订完成的整本 IR
  240 + base_name: 文件名片段(来源于报告主题/标题)
  241 + timestamp: 时间戳字符串
  242 +
  243 + 返回:
  244 + Path: 生成的 HTML 文件路径
  245 + """
144 renderer = HTMLRenderer() 246 renderer = HTMLRenderer()
145 html_content = renderer.render(document_ir) 247 html_content = renderer.render(document_ir)
146 248
@@ -163,7 +265,18 @@ def render_html(document_ir, base_name, timestamp): @@ -163,7 +265,18 @@ def render_html(document_ir, base_name, timestamp):
163 265
164 266
165 def build_slug(text): 267 def build_slug(text):
166 - """将主题/标题转换为安全的文件名片段。""" 268 + """
  269 + 将主题/标题转换为文件系统安全的片段。
  270 +
  271 + 仅保留字母/数字/空格/下划线/连字符,空格统一为下划线,并限制
  272 + 最长 60 字符,避免过长文件名。
  273 +
  274 + 参数:
  275 + text: 原始主题或标题
  276 +
  277 + 返回:
  278 + str: 清洗后的安全字符串
  279 + """
167 text = str(text or "report") 280 text = str(text or "report")
168 sanitized = "".join(c for c in text if c.isalnum() or c in (" ", "-", "_")).strip() 281 sanitized = "".join(c for c in text if c.isalnum() or c in (" ", "-", "_")).strip()
169 sanitized = sanitized.replace(" ", "_") 282 sanitized = sanitized.replace(" ", "_")
@@ -171,7 +284,18 @@ def build_slug(text): @@ -171,7 +284,18 @@ def build_slug(text):
171 284
172 285
173 def main(): 286 def main():
174 - """主入口:装订最新章节并渲染HTML。""" 287 + """
  288 + 主入口:读取最新章节、装订 IR 并渲染 HTML。
  289 +
  290 + 流程:
  291 + 1) 找到最新的章节 run 目录并读取 manifest;
  292 + 2) 加载章节并执行结构校验(仅警告);
  293 + 3) 装订整本 IR,保存 IR 副本;
  294 + 4) 渲染 HTML 并输出路径与统计信息。
  295 +
  296 + 返回:
  297 + int: 0 表示成功,其余表示失败。
  298 + """
175 logger.info("🚀 使用最新的LLM章节重新装订并渲染HTML") 299 logger.info("🚀 使用最新的LLM章节重新装订并渲染HTML")
176 300
177 chapter_root = Path(settings.CHAPTER_OUTPUT_DIR) 301 chapter_root = Path(settings.CHAPTER_OUTPUT_DIR)
@@ -14,7 +14,14 @@ sys.path.insert(0, str(Path(__file__).parent)) @@ -14,7 +14,14 @@ sys.path.insert(0, str(Path(__file__).parent))
14 from ReportEngine.renderers import PDFRenderer 14 from ReportEngine.renderers import PDFRenderer
15 15
16 def find_latest_report(): 16 def find_latest_report():
17 - """找到最新的报告IR文件""" 17 + """
  18 + 在 `final_reports/ir` 中查找最新的报告 IR JSON。
  19 +
  20 + 按修改时间倒序选择第一条,若目录或文件缺失则记录错误并返回 None。
  21 +
  22 + 返回:
  23 + Path | None: 最新 IR 文件路径;未找到则为 None。
  24 + """
18 ir_dir = Path("final_reports/ir") 25 ir_dir = Path("final_reports/ir")
19 26
20 if not ir_dir.exists(): 27 if not ir_dir.exists():
@@ -34,7 +41,18 @@ def find_latest_report(): @@ -34,7 +41,18 @@ def find_latest_report():
34 return latest_file 41 return latest_file
35 42
36 def load_document_ir(file_path): 43 def load_document_ir(file_path):
37 - """加载Document IR""" 44 + """
  45 + 读取指定路径的 Document IR JSON,并统计章节/图表数量。
  46 +
  47 + 解析失败时返回 None;成功时会打印章节数与图表数,便于确认
  48 + 输入报告的规模。
  49 +
  50 + 参数:
  51 + file_path: IR 文件路径
  52 +
  53 + 返回:
  54 + dict | None: 解析后的 Document IR;失败返回 None。
  55 + """
38 try: 56 try:
39 with open(file_path, 'r', encoding='utf-8') as f: 57 with open(file_path, 'r', encoding='utf-8') as f:
40 document_ir = json.load(f) 58 document_ir = json.load(f)
@@ -46,6 +64,7 @@ def load_document_ir(file_path): @@ -46,6 +64,7 @@ def load_document_ir(file_path):
46 chapters = document_ir.get('chapters', []) 64 chapters = document_ir.get('chapters', [])
47 65
48 def count_charts(blocks): 66 def count_charts(blocks):
  67 + """递归统计 block 列表中的 Chart.js 图表数量"""
49 count = 0 68 count = 0
50 for block in blocks: 69 for block in blocks:
51 if isinstance(block, dict): 70 if isinstance(block, dict):
@@ -70,7 +89,18 @@ def load_document_ir(file_path): @@ -70,7 +89,18 @@ def load_document_ir(file_path):
70 return None 89 return None
71 90
72 def generate_pdf_with_vector_charts(document_ir, output_path): 91 def generate_pdf_with_vector_charts(document_ir, output_path):
73 - """使用SVG矢量图表生成PDF""" 92 + """
  93 + 使用 PDFRenderer 将 Document IR 渲染为包含 SVG 矢量图表的 PDF。
  94 +
  95 + 启用布局优化,生成后输出文件大小与成功提示;异常时返回 None。
  96 +
  97 + 参数:
  98 + document_ir: 完整的 Document IR
  99 + output_path: 目标 PDF 路径
  100 +
  101 + 返回:
  102 + Path | None: 成功时返回生成的 PDF 路径,失败返回 None。
  103 + """
74 try: 104 try:
75 logger.info("=" * 60) 105 logger.info("=" * 60)
76 logger.info("开始生成PDF(带矢量图表)") 106 logger.info("开始生成PDF(带矢量图表)")
@@ -102,7 +132,18 @@ def generate_pdf_with_vector_charts(document_ir, output_path): @@ -102,7 +132,18 @@ def generate_pdf_with_vector_charts(document_ir, output_path):
102 return None 132 return None
103 133
104 def main(): 134 def main():
105 - """主函数""" 135 + """
  136 + 主入口:重新生成最新报告的矢量 PDF。
  137 +
  138 + 步骤:
  139 + 1) 查找最新 IR 文件;
  140 + 2) 读取并统计报告结构;
  141 + 3) 构造输出文件名并确保目录存在;
  142 + 4) 调用渲染函数生成 PDF,输出路径与特性说明。
  143 +
  144 + 返回:
  145 + int: 0 表示成功,非 0 表示失败。
  146 + """
106 logger.info("🚀 使用SVG矢量图表重新生成最新报告的PDF") 147 logger.info("🚀 使用SVG矢量图表重新生成最新报告的PDF")
107 logger.info("") 148 logger.info("")
108 149