From 4e882560daf26b0880cb96d190872eb29633520b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E4=B8=80=E4=B8=81?= <1769123563@qq.com> Date: Thu, 27 Nov 2025 09:51:42 +0800 Subject: [PATCH] Add Comments --- ReportEngine/nodes/chapter_generation_node.py | 2 + ReportEngine/renderers/chart_to_svg.py | 1 + ReportEngine/renderers/html_renderer.py | 2 + ReportEngine/renderers/pdf_renderer.py | 2 + ReportEngine/utils/chart_validator.py | 4 +- ReportEngine/utils/dependency_check.py | 2 + regenerate_latest_html.py | 146 ++++++++++++++++-- regenerate_latest_pdf.py | 49 +++++- 8 files changed, 192 insertions(+), 16 deletions(-) diff --git a/ReportEngine/nodes/chapter_generation_node.py b/ReportEngine/nodes/chapter_generation_node.py index 9ec3e34..76e6841 100644 --- a/ReportEngine/nodes/chapter_generation_node.py +++ b/ReportEngine/nodes/chapter_generation_node.py @@ -64,6 +64,7 @@ class ChapterContentError(ValueError): narrative_characters: int = 0, non_heading_blocks: int = 0, ): + """保存本次异常的正文特征,供重试与兜底策略参考。""" super().__init__(message) self.chapter_payload: Optional[Dict[str, Any]] = chapter self.body_characters: int = int(body_characters or 0) @@ -1018,6 +1019,7 @@ class ChapterGenerationNode(BaseNode): """ def walk(node: Any) -> int: + """递归遍历叙述性节点,忽略图表/目录等非正文结构""" if node is None: return 0 if isinstance(node, list): diff --git a/ReportEngine/renderers/chart_to_svg.py b/ReportEngine/renderers/chart_to_svg.py index 1b1b9fd..c7b93e4 100644 --- a/ReportEngine/renderers/chart_to_svg.py +++ b/ReportEngine/renderers/chart_to_svg.py @@ -797,6 +797,7 @@ class ChartToSVGConverter: colors = self._get_colors(datasets) def _safe_radius(raw) -> float: + """将输入半径安全转为浮点并设置最小阈值,避免气泡完全消失""" try: val = float(raw) return max(val, 0.5) diff --git a/ReportEngine/renderers/html_renderer.py b/ReportEngine/renderers/html_renderer.py index 730e79e..4e7c8b9 100644 --- a/ReportEngine/renderers/html_renderer.py +++ b/ReportEngine/renderers/html_renderer.py @@ -1764,6 +1764,7 @@ class HTMLRenderer: ) -> str: """为词云提供表格兜底,避免WordCloud渲染失败后页面空白""" def _collect_items(raw: Any) -> list[dict]: + """将多种词云输入格式(数组/对象/元组/纯文本)规整为统一的词条列表""" collected: list[dict] = [] if isinstance(raw, list): for item in raw: @@ -1812,6 +1813,7 @@ class HTMLRenderer: return "" def _format_weight(value: Any) -> str: + """统一格式化权重,支持百分比/数值与字符串回退""" if isinstance(value, (int, float)) and not isinstance(value, bool): if 0 <= value <= 1.5: return f"{value * 100:.1f}%" diff --git a/ReportEngine/renderers/pdf_renderer.py b/ReportEngine/renderers/pdf_renderer.py index 3bc7089..1347677 100644 --- a/ReportEngine/renderers/pdf_renderer.py +++ b/ReportEngine/renderers/pdf_renderer.py @@ -667,6 +667,7 @@ class PDFRenderer: fallback_pattern = rf'
]*data-widget-id="{re.escape(widget_id)}"[^>]*)>' def _hide_fallback(m: re.Match) -> str: + """为匹配到的图表fallback添加隐藏类,防止PDF中重复渲染""" tag = m.group(0) if 'svg-hidden' in tag: return tag @@ -712,6 +713,7 @@ class PDFRenderer: fallback_pattern = rf'
]*data-widget-id="{re.escape(widget_id)}"[^>]*)>' def _hide_fallback(m: re.Match) -> str: + """匹配词云表格兜底并打上隐藏标记,避免SVG/图片重复显示""" tag = m.group(0) if 'svg-hidden' in tag: return tag diff --git a/ReportEngine/utils/chart_validator.py b/ReportEngine/utils/chart_validator.py index 4743f14..ebf3d53 100644 --- a/ReportEngine/utils/chart_validator.py +++ b/ReportEngine/utils/chart_validator.py @@ -87,7 +87,7 @@ class ChartValidator: } def __init__(self): - pass + """初始化验证器并预留缓存结构,便于后续复用验证/修复结果""" def validate(self, widget_block: Dict[str, Any]) -> ValidationResult: """ @@ -136,6 +136,7 @@ class ChartValidator: # 检测是否使用了{x, y}形式的数据点(通常用于时间轴/散点) def contains_object_points(ds_list: List[Any] | None) -> bool: + """检查数据集中是否包含以x/y键表示的对象点,用于切换验证分支""" if not isinstance(ds_list, list): return False for point in ds_list: @@ -432,6 +433,7 @@ class ChartRepairer: return copy.deepcopy(cached) def _cache_and_return(res: RepairResult) -> RepairResult: + """写入修复结果缓存并返回,避免重复调用下游修复逻辑""" try: self._result_cache[cache_key] = copy.deepcopy(res) except Exception: diff --git a/ReportEngine/utils/dependency_check.py b/ReportEngine/utils/dependency_check.py index e926498..92647e4 100644 --- a/ReportEngine/utils/dependency_check.py +++ b/ReportEngine/utils/dependency_check.py @@ -27,6 +27,7 @@ def _get_platform_specific_instructions(): system = platform.system() def _box_lines(lines): + """批量将多行文本包装成带边框的提示块""" return "".join(_box_line(line) for line in lines) if system == "Darwin": # macOS @@ -107,6 +108,7 @@ def _ensure_windows_gtk_paths(): seen = set() def _add_candidate(path_like): + """收集可能的GTK安装路径,避免重复并兼容用户自定义目录""" if not path_like: return p = Path(path_like) diff --git a/regenerate_latest_html.py b/regenerate_latest_html.py index fe8e5f7..e695ca5 100644 --- a/regenerate_latest_html.py +++ b/regenerate_latest_html.py @@ -18,7 +18,19 @@ from ReportEngine.utils.config import settings def find_latest_run_dir(chapter_root: Path): - """定位包含 manifest.json 的最新章节输出目录。""" + """ + 定位章节根目录下最新一次运行的输出目录。 + + 扫描 `chapter_root` 下所有子目录,筛选出包含 `manifest.json` + 的候选,按修改时间倒序取最新一条。若目录不存在或没有有效 + manifest,会记录错误并返回 None。 + + 参数: + chapter_root: 章节输出的根目录(通常是 settings.CHAPTER_OUTPUT_DIR) + + 返回: + Path | None: 最新的 run 目录路径;若未找到则为 None。 + """ if not chapter_root.exists(): logger.error(f"章节目录不存在: {chapter_root}") return None @@ -41,7 +53,18 @@ def find_latest_run_dir(chapter_root: Path): def load_manifest(run_dir: Path): - """读取manifest.json并返回report_id与metadata。""" + """ + 读取单次运行目录内的 manifest.json。 + + 成功时返回 reportId 以及元数据字典;读取或解析失败会记录错误 + 并返回 (None, None),以便上层提前终止流程。 + + 参数: + run_dir: 包含 manifest.json 的章节输出目录 + + 返回: + tuple[str | None, dict | None]: (report_id, metadata) + """ manifest_path = run_dir / "manifest.json" try: with manifest_path.open("r", encoding="utf-8") as f: @@ -58,7 +81,18 @@ def load_manifest(run_dir: Path): def load_chapters(run_dir: Path): - """加载章节JSON列表。""" + """ + 读取指定 run 目录下的所有章节 JSON。 + + 会复用 ChapterStorage 的 load_chapters 能力,自动按 order 排序。 + 读取后打印章节数量,便于确认完整性。 + + 参数: + run_dir: 单次报告的章节目录 + + 返回: + list[dict]: 章节 JSON 列表(若目录为空则为空列表) + """ storage = ChapterStorage(settings.CHAPTER_OUTPUT_DIR) chapters = storage.load_chapters(run_dir) logger.info(f"加载章节数: {len(chapters)}") @@ -66,7 +100,15 @@ def load_chapters(run_dir: Path): def validate_chapters(chapters): - """使用IRValidator做快速校验,仅记录警告不阻断流程。""" + """ + 使用 IRValidator 对章节结构做快速校验。 + + 仅记录未通过的章节及前三条错误,不会中断流程;目的是在 + 重装订前发现潜在结构问题。 + + 参数: + chapters: 章节 JSON 列表 + """ validator = IRValidator() invalid = [] for chapter in chapters: @@ -84,7 +126,20 @@ def validate_chapters(chapters): def stitch_document(report_id, metadata, chapters): - """将章节装订为整本Document IR。""" + """ + 将各章节与元数据装订为完整的 Document IR。 + + 使用 DocumentComposer 统一处理章节顺序、全局元数据等,并打印 + 装订完成的章节与图表数量。 + + 参数: + report_id: 报告 ID(来自 manifest 或目录名) + metadata: manifest 中的全局元数据 + chapters: 已加载的章节列表 + + 返回: + dict: 完整的 Document IR 对象 + """ composer = DocumentComposer() document_ir = composer.build_document(report_id, metadata, chapters) logger.info( @@ -95,7 +150,18 @@ def stitch_document(report_id, metadata, chapters): def count_charts(document_ir): - """统计IR中的图表数量。""" + """ + 统计整本 Document IR 中的 Chart.js 图表数量。 + + 会遍历每章的 blocks,递归查找 widget 类型中以 `chart.js` + 开头的组件,便于快速感知图表规模。 + + 参数: + document_ir: 完整的 Document IR + + 返回: + int: 图表总数 + """ chart_count = 0 for chapter in document_ir.get("chapters", []): blocks = chapter.get("blocks", []) @@ -104,7 +170,17 @@ def count_charts(document_ir): def _count_chart_blocks(blocks): - """递归统计chart.js组件。""" + """ + 递归统计 block 列表中的 Chart.js 组件数量。 + + 兼容嵌套的 blocks/list/table 结构,确保所有层级的图表都被计入。 + + 参数: + blocks: 任意层级的 block 列表 + + 返回: + int: 统计到的 chart.js 图表数量 + """ count = 0 for block in blocks: if not isinstance(block, dict): @@ -129,7 +205,20 @@ def _count_chart_blocks(blocks): def save_document_ir(document_ir, base_name, timestamp): - """将装订好的IR重新落盘,便于后续复用。""" + """ + 将重新装订好的整本 Document IR 落盘。 + + 按 `report_ir_{slug}_{timestamp}_regen.json` 命名写入 + `settings.DOCUMENT_IR_OUTPUT_DIR`,确保目录存在并返回保存路径。 + + 参数: + document_ir: 已装订完成的整本 IR + base_name: 由主题/标题生成的安全文件名片段 + timestamp: 时间戳字符串,用于区分多次重生成 + + 返回: + Path: 保存的 IR 文件路径 + """ output_dir = Path(settings.DOCUMENT_IR_OUTPUT_DIR) output_dir.mkdir(parents=True, exist_ok=True) ir_filename = f"report_ir_{base_name}_{timestamp}_regen.json" @@ -140,7 +229,20 @@ def save_document_ir(document_ir, base_name, timestamp): def render_html(document_ir, base_name, timestamp): - """使用HTMLRenderer渲染并落盘HTML文件。""" + """ + 使用 HTMLRenderer 将 Document IR 渲染为 HTML 并保存。 + + 渲染后落盘到 `final_reports/html`,打印图表验证统计信息,方便 + 观察 Chart.js 数据的修复/失败情况。 + + 参数: + document_ir: 装订完成的整本 IR + base_name: 文件名片段(来源于报告主题/标题) + timestamp: 时间戳字符串 + + 返回: + Path: 生成的 HTML 文件路径 + """ renderer = HTMLRenderer() html_content = renderer.render(document_ir) @@ -163,7 +265,18 @@ def render_html(document_ir, base_name, timestamp): def build_slug(text): - """将主题/标题转换为安全的文件名片段。""" + """ + 将主题/标题转换为文件系统安全的片段。 + + 仅保留字母/数字/空格/下划线/连字符,空格统一为下划线,并限制 + 最长 60 字符,避免过长文件名。 + + 参数: + text: 原始主题或标题 + + 返回: + str: 清洗后的安全字符串 + """ text = str(text or "report") sanitized = "".join(c for c in text if c.isalnum() or c in (" ", "-", "_")).strip() sanitized = sanitized.replace(" ", "_") @@ -171,7 +284,18 @@ def build_slug(text): def main(): - """主入口:装订最新章节并渲染HTML。""" + """ + 主入口:读取最新章节、装订 IR 并渲染 HTML。 + + 流程: + 1) 找到最新的章节 run 目录并读取 manifest; + 2) 加载章节并执行结构校验(仅警告); + 3) 装订整本 IR,保存 IR 副本; + 4) 渲染 HTML 并输出路径与统计信息。 + + 返回: + int: 0 表示成功,其余表示失败。 + """ logger.info("🚀 使用最新的LLM章节重新装订并渲染HTML") chapter_root = Path(settings.CHAPTER_OUTPUT_DIR) diff --git a/regenerate_latest_pdf.py b/regenerate_latest_pdf.py index 4b68fb7..c2867a4 100644 --- a/regenerate_latest_pdf.py +++ b/regenerate_latest_pdf.py @@ -14,7 +14,14 @@ sys.path.insert(0, str(Path(__file__).parent)) from ReportEngine.renderers import PDFRenderer def find_latest_report(): - """找到最新的报告IR文件""" + """ + 在 `final_reports/ir` 中查找最新的报告 IR JSON。 + + 按修改时间倒序选择第一条,若目录或文件缺失则记录错误并返回 None。 + + 返回: + Path | None: 最新 IR 文件路径;未找到则为 None。 + """ ir_dir = Path("final_reports/ir") if not ir_dir.exists(): @@ -34,7 +41,18 @@ def find_latest_report(): return latest_file def load_document_ir(file_path): - """加载Document IR""" + """ + 读取指定路径的 Document IR JSON,并统计章节/图表数量。 + + 解析失败时返回 None;成功时会打印章节数与图表数,便于确认 + 输入报告的规模。 + + 参数: + file_path: IR 文件路径 + + 返回: + dict | None: 解析后的 Document IR;失败返回 None。 + """ try: with open(file_path, 'r', encoding='utf-8') as f: document_ir = json.load(f) @@ -46,6 +64,7 @@ def load_document_ir(file_path): chapters = document_ir.get('chapters', []) def count_charts(blocks): + """递归统计 block 列表中的 Chart.js 图表数量""" count = 0 for block in blocks: if isinstance(block, dict): @@ -70,7 +89,18 @@ def load_document_ir(file_path): return None def generate_pdf_with_vector_charts(document_ir, output_path): - """使用SVG矢量图表生成PDF""" + """ + 使用 PDFRenderer 将 Document IR 渲染为包含 SVG 矢量图表的 PDF。 + + 启用布局优化,生成后输出文件大小与成功提示;异常时返回 None。 + + 参数: + document_ir: 完整的 Document IR + output_path: 目标 PDF 路径 + + 返回: + Path | None: 成功时返回生成的 PDF 路径,失败返回 None。 + """ try: logger.info("=" * 60) logger.info("开始生成PDF(带矢量图表)") @@ -102,7 +132,18 @@ def generate_pdf_with_vector_charts(document_ir, output_path): return None def main(): - """主函数""" + """ + 主入口:重新生成最新报告的矢量 PDF。 + + 步骤: + 1) 查找最新 IR 文件; + 2) 读取并统计报告结构; + 3) 构造输出文件名并确保目录存在; + 4) 调用渲染函数生成 PDF,输出路径与特性说明。 + + 返回: + int: 0 表示成功,非 0 表示失败。 + """ logger.info("🚀 使用SVG矢量图表重新生成最新报告的PDF") logger.info("")