Showing
8 changed files
with
192 additions
and
16 deletions
| @@ -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 |
-
Please register or login to post a comment