Race condition in singleton ChartReviewService with concurrent renderers
Showing
5 changed files
with
138 additions
and
59 deletions
| @@ -294,16 +294,17 @@ class HTMLRenderer: | @@ -294,16 +294,17 @@ class HTMLRenderer: | ||
| 294 | 294 | ||
| 295 | # 使用统一的 ChartReviewService 进行图表审查与修复 | 295 | # 使用统一的 ChartReviewService 进行图表审查与修复 |
| 296 | # 修复结果会直接回写到 document_ir,避免多次渲染重复修复 | 296 | # 修复结果会直接回写到 document_ir,避免多次渲染重复修复 |
| 297 | + # review_document 返回本次会话的统计信息(线程安全) | ||
| 297 | chart_service = get_chart_review_service() | 298 | chart_service = get_chart_review_service() |
| 298 | - chart_service.review_document( | 299 | + review_stats = chart_service.review_document( |
| 299 | self.document, | 300 | self.document, |
| 300 | ir_file_path=ir_file_path, | 301 | ir_file_path=ir_file_path, |
| 301 | reset_stats=True, | 302 | reset_stats=True, |
| 302 | save_on_repair=bool(ir_file_path) | 303 | save_on_repair=bool(ir_file_path) |
| 303 | ) | 304 | ) |
| 304 | # 同步统计信息到本地(用于兼容旧的 _log_chart_validation_stats) | 305 | # 同步统计信息到本地(用于兼容旧的 _log_chart_validation_stats) |
| 305 | - service_stats = chart_service.stats | ||
| 306 | - self.chart_validation_stats.update(service_stats) | 306 | + # 使用返回的 ReviewStats 对象,而非共享的 chart_service.stats |
| 307 | + self.chart_validation_stats.update(review_stats.to_dict()) | ||
| 307 | 308 | ||
| 308 | self.widget_scripts = [] | 309 | self.widget_scripts = [] |
| 309 | self.chart_counter = 0 | 310 | self.chart_counter = 0 |
| @@ -40,8 +40,9 @@ class MarkdownRenderer: | @@ -40,8 +40,9 @@ class MarkdownRenderer: | ||
| 40 | 40 | ||
| 41 | # 使用统一的 ChartReviewService 进行图表审查与修复 | 41 | # 使用统一的 ChartReviewService 进行图表审查与修复 |
| 42 | # 虽然 Markdown 渲染时图表会降级为表格,但仍需确保数据有效 | 42 | # 虽然 Markdown 渲染时图表会降级为表格,但仍需确保数据有效 |
| 43 | + # review_document 返回本次会话的统计信息(线程安全,此处不使用) | ||
| 43 | chart_service = get_chart_review_service() | 44 | chart_service = get_chart_review_service() |
| 44 | - chart_service.review_document( | 45 | + _ = chart_service.review_document( |
| 45 | self.document, | 46 | self.document, |
| 46 | ir_file_path=ir_file_path, | 47 | ir_file_path=ir_file_path, |
| 47 | reset_stats=True, | 48 | reset_stats=True, |
| @@ -173,22 +173,22 @@ class PDFRenderer: | @@ -173,22 +173,22 @@ class PDFRenderer: | ||
| 173 | Dict[str, Any]: 修复后的Document IR(深拷贝) | 173 | Dict[str, Any]: 修复后的Document IR(深拷贝) |
| 174 | """ | 174 | """ |
| 175 | # 使用统一的 ChartReviewService | 175 | # 使用统一的 ChartReviewService |
| 176 | + # review_document 返回本次会话的统计信息(线程安全) | ||
| 176 | chart_service = get_chart_review_service() | 177 | chart_service = get_chart_review_service() |
| 177 | - chart_service.review_document( | 178 | + review_stats = chart_service.review_document( |
| 178 | document_ir, | 179 | document_ir, |
| 179 | ir_file_path=ir_file_path, | 180 | ir_file_path=ir_file_path, |
| 180 | reset_stats=True, | 181 | reset_stats=True, |
| 181 | save_on_repair=bool(ir_file_path) | 182 | save_on_repair=bool(ir_file_path) |
| 182 | ) | 183 | ) |
| 183 | 184 | ||
| 184 | - stats = chart_service.stats | ||
| 185 | - if stats.get('total', 0) > 0: | ||
| 186 | - repaired_count = stats.get('repaired_locally', 0) + stats.get('repaired_api', 0) | 185 | + # 使用返回的 ReviewStats 对象,而非共享的 chart_service.stats |
| 186 | + if review_stats.total > 0: | ||
| 187 | logger.info( | 187 | logger.info( |
| 188 | f"PDF图表预处理完成: " | 188 | f"PDF图表预处理完成: " |
| 189 | - f"总计 {stats.get('total', 0)} 个图表, " | ||
| 190 | - f"修复 {repaired_count} 个, " | ||
| 191 | - f"失败 {stats.get('failed', 0)} 个" | 189 | + f"总计 {review_stats.total} 个图表, " |
| 190 | + f"修复 {review_stats.repaired_total} 个, " | ||
| 191 | + f"失败 {review_stats.failed} 个" | ||
| 192 | ) | 192 | ) |
| 193 | 193 | ||
| 194 | # 返回深拷贝,避免后续 SVG 转换过程影响回写后的原始 IR | 194 | # 返回深拷贝,避免后续 SVG 转换过程影响回写后的原始 IR |
| @@ -6,6 +6,7 @@ Report Engine工具模块。 | @@ -6,6 +6,7 @@ Report Engine工具模块。 | ||
| 6 | 6 | ||
| 7 | from ReportEngine.utils.chart_review_service import ( | 7 | from ReportEngine.utils.chart_review_service import ( |
| 8 | ChartReviewService, | 8 | ChartReviewService, |
| 9 | + ReviewStats, | ||
| 9 | get_chart_review_service, | 10 | get_chart_review_service, |
| 10 | review_document_charts, | 11 | review_document_charts, |
| 11 | ) | 12 | ) |
| @@ -21,6 +22,7 @@ from ReportEngine.utils.table_validator import ( | @@ -21,6 +22,7 @@ from ReportEngine.utils.table_validator import ( | ||
| 21 | 22 | ||
| 22 | __all__ = [ | 23 | __all__ = [ |
| 23 | "ChartReviewService", | 24 | "ChartReviewService", |
| 25 | + "ReviewStats", | ||
| 24 | "get_chart_review_service", | 26 | "get_chart_review_service", |
| 25 | "review_document_charts", | 27 | "review_document_charts", |
| 26 | "TableValidator", | 28 | "TableValidator", |
| @@ -3,6 +3,11 @@ | @@ -3,6 +3,11 @@ | ||
| 3 | 3 | ||
| 4 | 提供单例服务,确保所有渲染器共享修复状态,避免重复修复。 | 4 | 提供单例服务,确保所有渲染器共享修复状态,避免重复修复。 |
| 5 | 修复成功后可自动持久化到 IR 文件。 | 5 | 修复成功后可自动持久化到 IR 文件。 |
| 6 | + | ||
| 7 | +线程安全说明: | ||
| 8 | +- 验证器和修复器实例是无状态的,可安全共享 | ||
| 9 | +- 每次 review_document 调用会创建独立的 ReviewSession | ||
| 10 | +- 统计信息通过 ReviewSession 返回,避免并发竞争 | ||
| 6 | """ | 11 | """ |
| 7 | 12 | ||
| 8 | from __future__ import annotations | 13 | from __future__ import annotations |
| @@ -10,6 +15,7 @@ from __future__ import annotations | @@ -10,6 +15,7 @@ from __future__ import annotations | ||
| 10 | import copy | 15 | import copy |
| 11 | import json | 16 | import json |
| 12 | import threading | 17 | import threading |
| 18 | +from dataclasses import dataclass | ||
| 13 | from pathlib import Path | 19 | from pathlib import Path |
| 14 | from typing import Any, Dict, List, Optional | 20 | from typing import Any, Dict, List, Optional |
| 15 | 21 | ||
| @@ -25,6 +31,36 @@ from ReportEngine.utils.chart_validator import ( | @@ -25,6 +31,36 @@ from ReportEngine.utils.chart_validator import ( | ||
| 25 | from ReportEngine.utils.chart_repair_api import create_llm_repair_functions | 31 | from ReportEngine.utils.chart_repair_api import create_llm_repair_functions |
| 26 | 32 | ||
| 27 | 33 | ||
| 34 | +@dataclass | ||
| 35 | +class ReviewStats: | ||
| 36 | + """ | ||
| 37 | + 图表审查统计信息 - 每次审查会话独立的统计数据。 | ||
| 38 | + | ||
| 39 | + 通过为每次 review_document 调用创建独立的 ReviewStats 实例, | ||
| 40 | + 避免多线程并发时的统计数据竞争问题。 | ||
| 41 | + """ | ||
| 42 | + total: int = 0 | ||
| 43 | + valid: int = 0 | ||
| 44 | + repaired_locally: int = 0 | ||
| 45 | + repaired_api: int = 0 | ||
| 46 | + failed: int = 0 | ||
| 47 | + | ||
| 48 | + def to_dict(self) -> Dict[str, int]: | ||
| 49 | + """转换为字典格式""" | ||
| 50 | + return { | ||
| 51 | + 'total': self.total, | ||
| 52 | + 'valid': self.valid, | ||
| 53 | + 'repaired_locally': self.repaired_locally, | ||
| 54 | + 'repaired_api': self.repaired_api, | ||
| 55 | + 'failed': self.failed | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + @property | ||
| 59 | + def repaired_total(self) -> int: | ||
| 60 | + """修复总数""" | ||
| 61 | + return self.repaired_locally + self.repaired_api | ||
| 62 | + | ||
| 63 | + | ||
| 28 | class ChartReviewService: | 64 | class ChartReviewService: |
| 29 | """ | 65 | """ |
| 30 | 图表审查服务 - 单例模式。 | 66 | 图表审查服务 - 单例模式。 |
| @@ -33,7 +69,12 @@ class ChartReviewService: | @@ -33,7 +69,12 @@ class ChartReviewService: | ||
| 33 | 1. 统一管理图表验证和修复 | 69 | 1. 统一管理图表验证和修复 |
| 34 | 2. 维护修复缓存,避免重复修复 | 70 | 2. 维护修复缓存,避免重复修复 |
| 35 | 3. 支持修复后自动持久化到 IR 文件 | 71 | 3. 支持修复后自动持久化到 IR 文件 |
| 36 | - 4. 提供统计信息 | 72 | + 4. 提供统计信息(通过 ReviewStats 返回,线程安全) |
| 73 | + | ||
| 74 | + 线程安全说明: | ||
| 75 | + - validator 和 repairer 是无状态的,可安全共享 | ||
| 76 | + - 每次 review_document 调用创建独立的 ReviewStats | ||
| 77 | + - 不再使用全局 _stats,避免并发竞争 | ||
| 37 | """ | 78 | """ |
| 38 | 79 | ||
| 39 | _instance: Optional["ChartReviewService"] = None | 80 | _instance: Optional["ChartReviewService"] = None |
| @@ -55,7 +96,7 @@ class ChartReviewService: | @@ -55,7 +96,7 @@ class ChartReviewService: | ||
| 55 | 96 | ||
| 56 | self._initialized = True | 97 | self._initialized = True |
| 57 | 98 | ||
| 58 | - # 初始化验证器和修复器 | 99 | + # 初始化验证器和修复器(无状态,可安全共享) |
| 59 | self.validator = create_chart_validator() | 100 | self.validator = create_chart_validator() |
| 60 | self.llm_repair_fns = create_llm_repair_functions() | 101 | self.llm_repair_fns = create_llm_repair_functions() |
| 61 | self.repairer = create_chart_repairer( | 102 | self.repairer = create_chart_repairer( |
| @@ -69,31 +110,44 @@ class ChartReviewService: | @@ -69,31 +110,44 @@ class ChartReviewService: | ||
| 69 | else: | 110 | else: |
| 70 | logger.info(f"ChartReviewService: 已配置 {len(self.llm_repair_fns)} 个 LLM 修复函数") | 111 | logger.info(f"ChartReviewService: 已配置 {len(self.llm_repair_fns)} 个 LLM 修复函数") |
| 71 | 112 | ||
| 72 | - # 统计信息 | ||
| 73 | - self._stats = { | ||
| 74 | - 'total': 0, | ||
| 75 | - 'valid': 0, | ||
| 76 | - 'repaired_locally': 0, | ||
| 77 | - 'repaired_api': 0, | ||
| 78 | - 'failed': 0 | ||
| 79 | - } | 113 | + # 最后一次审查的统计信息(仅用于向后兼容,不推荐在并发场景使用) |
| 114 | + # 新代码应使用 review_document 返回的 ReviewStats | ||
| 115 | + self._last_stats: Optional[ReviewStats] = None | ||
| 116 | + self._last_stats_lock = threading.Lock() | ||
| 80 | 117 | ||
| 81 | logger.info("ChartReviewService 初始化完成") | 118 | logger.info("ChartReviewService 初始化完成") |
| 82 | 119 | ||
| 83 | def reset_stats(self) -> None: | 120 | def reset_stats(self) -> None: |
| 84 | - """重置统计信息""" | ||
| 85 | - self._stats = { | 121 | + """ |
| 122 | + 重置统计信息(向后兼容,不推荐使用)。 | ||
| 123 | + | ||
| 124 | + 注意:此方法仅用于向后兼容。在并发场景下, | ||
| 125 | + 应使用 review_document 返回的 ReviewStats 对象。 | ||
| 126 | + """ | ||
| 127 | + with self._last_stats_lock: | ||
| 128 | + self._last_stats = None | ||
| 129 | + | ||
| 130 | + @property | ||
| 131 | + def stats(self) -> Dict[str, int]: | ||
| 132 | + """ | ||
| 133 | + 获取最后一次审查的统计信息副本(向后兼容)。 | ||
| 134 | + | ||
| 135 | + 警告:在并发场景下,此属性可能返回其他线程的统计结果。 | ||
| 136 | + 推荐使用 review_document 返回的 ReviewStats 对象。 | ||
| 137 | + | ||
| 138 | + 返回: | ||
| 139 | + Dict[str, int]: 统计信息字典副本 | ||
| 140 | + """ | ||
| 141 | + with self._last_stats_lock: | ||
| 142 | + if self._last_stats is None: | ||
| 143 | + return { | ||
| 86 | 'total': 0, | 144 | 'total': 0, |
| 87 | 'valid': 0, | 145 | 'valid': 0, |
| 88 | 'repaired_locally': 0, | 146 | 'repaired_locally': 0, |
| 89 | 'repaired_api': 0, | 147 | 'repaired_api': 0, |
| 90 | 'failed': 0 | 148 | 'failed': 0 |
| 91 | } | 149 | } |
| 92 | - | ||
| 93 | - @property | ||
| 94 | - def stats(self) -> Dict[str, int]: | ||
| 95 | - """获取统计信息副本""" | ||
| 96 | - return self._stats.copy() | 150 | + return self._last_stats.to_dict() |
| 97 | 151 | ||
| 98 | def review_document( | 152 | def review_document( |
| 99 | self, | 153 | self, |
| @@ -102,28 +156,33 @@ class ChartReviewService: | @@ -102,28 +156,33 @@ class ChartReviewService: | ||
| 102 | *, | 156 | *, |
| 103 | reset_stats: bool = True, | 157 | reset_stats: bool = True, |
| 104 | save_on_repair: bool = True | 158 | save_on_repair: bool = True |
| 105 | - ) -> Dict[str, Any]: | 159 | + ) -> ReviewStats: |
| 106 | """ | 160 | """ |
| 107 | 审查并修复文档中的所有图表。 | 161 | 审查并修复文档中的所有图表。 |
| 108 | 162 | ||
| 109 | 遍历所有章节的 blocks,检测图表类型的 widget, | 163 | 遍历所有章节的 blocks,检测图表类型的 widget, |
| 110 | 对未审查过的图表进行验证和修复。 | 164 | 对未审查过的图表进行验证和修复。 |
| 111 | 165 | ||
| 166 | + 线程安全:每次调用创建独立的 ReviewStats,避免并发竞争。 | ||
| 167 | + | ||
| 112 | 参数: | 168 | 参数: |
| 113 | document_ir: Document IR 数据 | 169 | document_ir: Document IR 数据 |
| 114 | ir_file_path: IR 文件路径,如果提供且有修复,会自动保存 | 170 | ir_file_path: IR 文件路径,如果提供且有修复,会自动保存 |
| 115 | - reset_stats: 是否重置统计信息 | 171 | + reset_stats: 保留参数以保持向后兼容,不再有实际作用 |
| 116 | save_on_repair: 修复后是否自动保存到文件 | 172 | save_on_repair: 修复后是否自动保存到文件 |
| 117 | 173 | ||
| 118 | 返回: | 174 | 返回: |
| 119 | - Dict[str, Any]: 审查后的 Document IR(原对象,已修改) | 175 | + ReviewStats: 本次审查的统计信息(线程安全) |
| 120 | """ | 176 | """ |
| 121 | - if reset_stats: | ||
| 122 | - self.reset_stats() | 177 | + # 每次调用创建独立的统计对象,避免并发竞争 |
| 178 | + session_stats = ReviewStats() | ||
| 123 | 179 | ||
| 124 | if not document_ir: | 180 | if not document_ir: |
| 125 | logger.warning("ChartReviewService: document_ir 为空,跳过审查") | 181 | logger.warning("ChartReviewService: document_ir 为空,跳过审查") |
| 126 | - return document_ir | 182 | + # 更新 _last_stats 以保持向后兼容 |
| 183 | + with self._last_stats_lock: | ||
| 184 | + self._last_stats = session_stats | ||
| 185 | + return session_stats | ||
| 127 | 186 | ||
| 128 | has_repairs = False | 187 | has_repairs = False |
| 129 | 188 | ||
| @@ -133,27 +192,37 @@ class ChartReviewService: | @@ -133,27 +192,37 @@ class ChartReviewService: | ||
| 133 | continue | 192 | continue |
| 134 | blocks = chapter.get("blocks", []) | 193 | blocks = chapter.get("blocks", []) |
| 135 | if isinstance(blocks, list): | 194 | if isinstance(blocks, list): |
| 136 | - chapter_repairs = self._walk_and_review_blocks(blocks, chapter) | 195 | + chapter_repairs = self._walk_and_review_blocks(blocks, chapter, session_stats) |
| 137 | if chapter_repairs: | 196 | if chapter_repairs: |
| 138 | has_repairs = True | 197 | has_repairs = True |
| 139 | 198 | ||
| 140 | # 输出统计信息 | 199 | # 输出统计信息 |
| 141 | - self._log_stats() | 200 | + self._log_stats(session_stats) |
| 201 | + | ||
| 202 | + # 更新 _last_stats 以保持向后兼容 | ||
| 203 | + with self._last_stats_lock: | ||
| 204 | + self._last_stats = session_stats | ||
| 142 | 205 | ||
| 143 | # 如果有修复且提供了文件路径,保存到文件 | 206 | # 如果有修复且提供了文件路径,保存到文件 |
| 144 | if has_repairs and ir_file_path and save_on_repair: | 207 | if has_repairs and ir_file_path and save_on_repair: |
| 145 | self._save_ir_to_file(document_ir, ir_file_path) | 208 | self._save_ir_to_file(document_ir, ir_file_path) |
| 146 | 209 | ||
| 147 | - return document_ir | 210 | + return session_stats |
| 148 | 211 | ||
| 149 | def _walk_and_review_blocks( | 212 | def _walk_and_review_blocks( |
| 150 | self, | 213 | self, |
| 151 | blocks: List[Any], | 214 | blocks: List[Any], |
| 152 | - chapter_context: Dict[str, Any] | None = None | 215 | + chapter_context: Dict[str, Any] | None, |
| 216 | + session_stats: ReviewStats | ||
| 153 | ) -> bool: | 217 | ) -> bool: |
| 154 | """ | 218 | """ |
| 155 | 递归遍历 blocks 并审查图表。 | 219 | 递归遍历 blocks 并审查图表。 |
| 156 | 220 | ||
| 221 | + 参数: | ||
| 222 | + blocks: 要遍历的 block 列表 | ||
| 223 | + chapter_context: 章节上下文 | ||
| 224 | + session_stats: 本次审查会话的统计对象 | ||
| 225 | + | ||
| 157 | 返回: | 226 | 返回: |
| 158 | bool: 是否有修复发生 | 227 | bool: 是否有修复发生 |
| 159 | """ | 228 | """ |
| @@ -165,21 +234,21 @@ class ChartReviewService: | @@ -165,21 +234,21 @@ class ChartReviewService: | ||
| 165 | 234 | ||
| 166 | # 检查是否是图表 widget | 235 | # 检查是否是图表 widget |
| 167 | if block.get("type") == "widget": | 236 | if block.get("type") == "widget": |
| 168 | - repaired = self._review_chart_block(block, chapter_context) | 237 | + repaired = self._review_chart_block(block, chapter_context, session_stats) |
| 169 | if repaired: | 238 | if repaired: |
| 170 | has_repairs = True | 239 | has_repairs = True |
| 171 | 240 | ||
| 172 | # 递归处理嵌套的 blocks | 241 | # 递归处理嵌套的 blocks |
| 173 | nested_blocks = block.get("blocks") | 242 | nested_blocks = block.get("blocks") |
| 174 | if isinstance(nested_blocks, list): | 243 | if isinstance(nested_blocks, list): |
| 175 | - if self._walk_and_review_blocks(nested_blocks, chapter_context): | 244 | + if self._walk_and_review_blocks(nested_blocks, chapter_context, session_stats): |
| 176 | has_repairs = True | 245 | has_repairs = True |
| 177 | 246 | ||
| 178 | # 处理 list 类型的 items | 247 | # 处理 list 类型的 items |
| 179 | if block.get("type") == "list": | 248 | if block.get("type") == "list": |
| 180 | for item in block.get("items", []): | 249 | for item in block.get("items", []): |
| 181 | if isinstance(item, list): | 250 | if isinstance(item, list): |
| 182 | - if self._walk_and_review_blocks(item, chapter_context): | 251 | + if self._walk_and_review_blocks(item, chapter_context, session_stats): |
| 183 | has_repairs = True | 252 | has_repairs = True |
| 184 | 253 | ||
| 185 | # 处理 table 类型的 cells | 254 | # 处理 table 类型的 cells |
| @@ -191,7 +260,7 @@ class ChartReviewService: | @@ -191,7 +260,7 @@ class ChartReviewService: | ||
| 191 | if isinstance(cell, dict): | 260 | if isinstance(cell, dict): |
| 192 | cell_blocks = cell.get("blocks", []) | 261 | cell_blocks = cell.get("blocks", []) |
| 193 | if isinstance(cell_blocks, list): | 262 | if isinstance(cell_blocks, list): |
| 194 | - if self._walk_and_review_blocks(cell_blocks, chapter_context): | 263 | + if self._walk_and_review_blocks(cell_blocks, chapter_context, session_stats): |
| 195 | has_repairs = True | 264 | has_repairs = True |
| 196 | 265 | ||
| 197 | return has_repairs | 266 | return has_repairs |
| @@ -199,11 +268,17 @@ class ChartReviewService: | @@ -199,11 +268,17 @@ class ChartReviewService: | ||
| 199 | def _review_chart_block( | 268 | def _review_chart_block( |
| 200 | self, | 269 | self, |
| 201 | block: Dict[str, Any], | 270 | block: Dict[str, Any], |
| 202 | - chapter_context: Dict[str, Any] | None = None | 271 | + chapter_context: Dict[str, Any] | None, |
| 272 | + session_stats: ReviewStats | ||
| 203 | ) -> bool: | 273 | ) -> bool: |
| 204 | """ | 274 | """ |
| 205 | 审查单个图表 block。 | 275 | 审查单个图表 block。 |
| 206 | 276 | ||
| 277 | + 参数: | ||
| 278 | + block: 要审查的 block | ||
| 279 | + chapter_context: 章节上下文 | ||
| 280 | + session_stats: 本次审查会话的统计对象 | ||
| 281 | + | ||
| 207 | 返回: | 282 | 返回: |
| 208 | bool: 是否进行了修复 | 283 | bool: 是否进行了修复 |
| 209 | """ | 284 | """ |
| @@ -225,11 +300,11 @@ class ChartReviewService: | @@ -225,11 +300,11 @@ class ChartReviewService: | ||
| 225 | logger.debug(f"图表 {widget_id} 已审查过,跳过") | 300 | logger.debug(f"图表 {widget_id} 已审查过,跳过") |
| 226 | return False | 301 | return False |
| 227 | 302 | ||
| 228 | - self._stats['total'] += 1 | 303 | + session_stats.total += 1 |
| 229 | 304 | ||
| 230 | # 词云直接标记为有效 | 305 | # 词云直接标记为有效 |
| 231 | if is_wordcloud: | 306 | if is_wordcloud: |
| 232 | - self._stats['valid'] += 1 | 307 | + session_stats.valid += 1 |
| 233 | block["_chart_reviewed"] = True | 308 | block["_chart_reviewed"] = True |
| 234 | block["_chart_review_status"] = "valid" | 309 | block["_chart_review_status"] = "valid" |
| 235 | block["_chart_review_method"] = "none" | 310 | block["_chart_review_method"] = "none" |
| @@ -243,7 +318,7 @@ class ChartReviewService: | @@ -243,7 +318,7 @@ class ChartReviewService: | ||
| 243 | 318 | ||
| 244 | if validation_result.is_valid: | 319 | if validation_result.is_valid: |
| 245 | # 验证通过 | 320 | # 验证通过 |
| 246 | - self._stats['valid'] += 1 | 321 | + session_stats.valid += 1 |
| 247 | block["_chart_reviewed"] = True | 322 | block["_chart_reviewed"] = True |
| 248 | block["_chart_review_status"] = "valid" | 323 | block["_chart_review_status"] = "valid" |
| 249 | block["_chart_review_method"] = "none" | 324 | block["_chart_review_method"] = "none" |
| @@ -269,9 +344,9 @@ class ChartReviewService: | @@ -269,9 +344,9 @@ class ChartReviewService: | ||
| 269 | 344 | ||
| 270 | method = repair_result.method or "local" | 345 | method = repair_result.method or "local" |
| 271 | if method == "local": | 346 | if method == "local": |
| 272 | - self._stats['repaired_locally'] += 1 | 347 | + session_stats.repaired_locally += 1 |
| 273 | elif method == "api": | 348 | elif method == "api": |
| 274 | - self._stats['repaired_api'] += 1 | 349 | + session_stats.repaired_api += 1 |
| 275 | 350 | ||
| 276 | block["_chart_reviewed"] = True | 351 | block["_chart_reviewed"] = True |
| 277 | block["_chart_review_status"] = "repaired" | 352 | block["_chart_review_status"] = "repaired" |
| @@ -281,7 +356,7 @@ class ChartReviewService: | @@ -281,7 +356,7 @@ class ChartReviewService: | ||
| 281 | return True | 356 | return True |
| 282 | 357 | ||
| 283 | # 修复失败 | 358 | # 修复失败 |
| 284 | - self._stats['failed'] += 1 | 359 | + session_stats.failed += 1 |
| 285 | block["_chart_reviewed"] = True | 360 | block["_chart_reviewed"] = True |
| 286 | block["_chart_renderable"] = False | 361 | block["_chart_renderable"] = False |
| 287 | block["_chart_review_status"] = "failed" | 362 | block["_chart_review_status"] = "failed" |
| @@ -412,19 +487,18 @@ class ChartReviewService: | @@ -412,19 +487,18 @@ class ChartReviewService: | ||
| 412 | return "验证失败但无具体错误信息" | 487 | return "验证失败但无具体错误信息" |
| 413 | return "; ".join(errors[:3]) | 488 | return "; ".join(errors[:3]) |
| 414 | 489 | ||
| 415 | - def _log_stats(self) -> None: | 490 | + def _log_stats(self, stats: ReviewStats) -> None: |
| 416 | """输出统计信息""" | 491 | """输出统计信息""" |
| 417 | - if self._stats['total'] == 0: | 492 | + if stats.total == 0: |
| 418 | logger.debug("ChartReviewService: 没有图表需要审查") | 493 | logger.debug("ChartReviewService: 没有图表需要审查") |
| 419 | return | 494 | return |
| 420 | 495 | ||
| 421 | - repaired = self._stats['repaired_locally'] + self._stats['repaired_api'] | ||
| 422 | logger.info( | 496 | logger.info( |
| 423 | f"ChartReviewService 图表审查完成: " | 497 | f"ChartReviewService 图表审查完成: " |
| 424 | - f"总计 {self._stats['total']} 个, " | ||
| 425 | - f"有效 {self._stats['valid']} 个, " | ||
| 426 | - f"修复 {repaired} 个 (本地 {self._stats['repaired_locally']}, API {self._stats['repaired_api']}), " | ||
| 427 | - f"失败 {self._stats['failed']} 个" | 498 | + f"总计 {stats.total} 个, " |
| 499 | + f"有效 {stats.valid} 个, " | ||
| 500 | + f"修复 {stats.repaired_total} 个 (本地 {stats.repaired_locally}, API {stats.repaired_api}), " | ||
| 501 | + f"失败 {stats.failed} 个" | ||
| 428 | ) | 502 | ) |
| 429 | 503 | ||
| 430 | # 内部元数据键,不应保存到 IR 文件 | 504 | # 内部元数据键,不应保存到 IR 文件 |
| @@ -526,18 +600,18 @@ def review_document_charts( | @@ -526,18 +600,18 @@ def review_document_charts( | ||
| 526 | *, | 600 | *, |
| 527 | reset_stats: bool = True, | 601 | reset_stats: bool = True, |
| 528 | save_on_repair: bool = True | 602 | save_on_repair: bool = True |
| 529 | -) -> Dict[str, Any]: | 603 | +) -> ReviewStats: |
| 530 | """ | 604 | """ |
| 531 | 便捷函数:审查并修复文档中的所有图表。 | 605 | 便捷函数:审查并修复文档中的所有图表。 |
| 532 | 606 | ||
| 533 | 参数: | 607 | 参数: |
| 534 | document_ir: Document IR 数据 | 608 | document_ir: Document IR 数据 |
| 535 | ir_file_path: IR 文件路径,如果提供且有修复,会自动保存 | 609 | ir_file_path: IR 文件路径,如果提供且有修复,会自动保存 |
| 536 | - reset_stats: 是否重置统计信息 | 610 | + reset_stats: 保留参数以保持向后兼容,不再有实际作用 |
| 537 | save_on_repair: 修复后是否自动保存到文件 | 611 | save_on_repair: 修复后是否自动保存到文件 |
| 538 | 612 | ||
| 539 | 返回: | 613 | 返回: |
| 540 | - Dict[str, Any]: 审查后的 Document IR | 614 | + ReviewStats: 本次审查的统计信息 |
| 541 | """ | 615 | """ |
| 542 | service = get_chart_review_service() | 616 | service = get_chart_review_service() |
| 543 | return service.review_document( | 617 | return service.review_document( |
| @@ -550,6 +624,7 @@ def review_document_charts( | @@ -550,6 +624,7 @@ def review_document_charts( | ||
| 550 | 624 | ||
| 551 | __all__ = [ | 625 | __all__ = [ |
| 552 | "ChartReviewService", | 626 | "ChartReviewService", |
| 627 | + "ReviewStats", | ||
| 553 | "get_chart_review_service", | 628 | "get_chart_review_service", |
| 554 | "review_document_charts", | 629 | "review_document_charts", |
| 555 | ] | 630 | ] |
-
Please register or login to post a comment