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("")