Incomplete chart normalization in "ChartReviewService" causes missing critical fields
Showing
1 changed file
with
82 additions
and
17 deletions
| @@ -297,30 +297,58 @@ class ChartReviewService: | @@ -297,30 +297,58 @@ class ChartReviewService: | ||
| 297 | chapter_context: Dict[str, Any] | None = None | 297 | chapter_context: Dict[str, Any] | None = None |
| 298 | ) -> None: | 298 | ) -> None: |
| 299 | """ | 299 | """ |
| 300 | - 规范化图表数据,从章节上下文补充缺失数据。 | 300 | + 规范化图表数据,补全缺失字段(如props、scales、datasets),提升容错性。 |
| 301 | + | ||
| 302 | + 与 HTMLRenderer._normalize_chart_block() 保持一致: | ||
| 303 | + - 确保 props 存在 | ||
| 304 | + - 将顶层 scales 合并进 props.options | ||
| 305 | + - 确保 data 存在 | ||
| 306 | + - 尝试使用章节级 data 作为兜底 | ||
| 307 | + - 自动生成 labels | ||
| 301 | """ | 308 | """ |
| 302 | if not isinstance(block, dict): | 309 | if not isinstance(block, dict): |
| 303 | return | 310 | return |
| 304 | 311 | ||
| 312 | + if block.get("type") != "widget": | ||
| 313 | + return | ||
| 314 | + | ||
| 315 | + widget_type = block.get("widgetType", "") | ||
| 316 | + if not (isinstance(widget_type, str) and widget_type.startswith("chart.js")): | ||
| 317 | + return | ||
| 318 | + | ||
| 319 | + # 确保 props 存在 | ||
| 320 | + props = block.get("props") | ||
| 321 | + if not isinstance(props, dict): | ||
| 322 | + block["props"] = {} | ||
| 323 | + props = block["props"] | ||
| 324 | + | ||
| 325 | + # 将顶层 scales 合并进 options,避免配置丢失 | ||
| 326 | + scales = block.get("scales") | ||
| 327 | + if isinstance(scales, dict): | ||
| 328 | + options = props.get("options") if isinstance(props.get("options"), dict) else {} | ||
| 329 | + props["options"] = self._merge_dicts(options, {"scales": scales}) | ||
| 330 | + | ||
| 331 | + # 确保 data 存在 | ||
| 305 | data = block.get("data") | 332 | data = block.get("data") |
| 306 | if not isinstance(data, dict): | 333 | if not isinstance(data, dict): |
| 307 | - return | 334 | + data = {} |
| 335 | + block["data"] = data | ||
| 308 | 336 | ||
| 309 | - # 尝试从章节上下文补充 datasets | ||
| 310 | - datasets = data.get("datasets") | ||
| 311 | - if not datasets or (isinstance(datasets, list) and len(datasets) == 0): | ||
| 312 | - if isinstance(chapter_context, dict): | ||
| 313 | - chapter_data = chapter_context.get("data") | ||
| 314 | - if isinstance(chapter_data, dict): | ||
| 315 | - fallback_ds = chapter_data.get("datasets") | ||
| 316 | - if isinstance(fallback_ds, list) and len(fallback_ds) > 0: | ||
| 317 | - merged_data = copy.deepcopy(data) | ||
| 318 | - merged_data["datasets"] = copy.deepcopy(fallback_ds) | ||
| 319 | - if not merged_data.get("labels") and isinstance(chapter_data.get("labels"), list): | ||
| 320 | - merged_data["labels"] = copy.deepcopy(chapter_data["labels"]) | ||
| 321 | - block["data"] = merged_data | ||
| 322 | - | ||
| 323 | - # 如果缺少 labels 且数据点包含 x 值,自动生成 | 337 | + # 如果 datasets 为空,尝试使用章节级 data 填充 |
| 338 | + if chapter_context and self._is_chart_data_empty(data): | ||
| 339 | + chapter_data = chapter_context.get("data") if isinstance(chapter_context, dict) else None | ||
| 340 | + if isinstance(chapter_data, dict): | ||
| 341 | + fallback_ds = chapter_data.get("datasets") | ||
| 342 | + if isinstance(fallback_ds, list) and len(fallback_ds) > 0: | ||
| 343 | + merged_data = copy.deepcopy(data) | ||
| 344 | + merged_data["datasets"] = copy.deepcopy(fallback_ds) | ||
| 345 | + | ||
| 346 | + if not merged_data.get("labels") and isinstance(chapter_data.get("labels"), list): | ||
| 347 | + merged_data["labels"] = copy.deepcopy(chapter_data["labels"]) | ||
| 348 | + | ||
| 349 | + block["data"] = merged_data | ||
| 350 | + | ||
| 351 | + # 若仍缺少 labels 且数据点包含 x 值,自动生成便于 fallback 和坐标刻度 | ||
| 324 | data_ref = block.get("data") | 352 | data_ref = block.get("data") |
| 325 | if isinstance(data_ref, dict) and not data_ref.get("labels"): | 353 | if isinstance(data_ref, dict) and not data_ref.get("labels"): |
| 326 | datasets_ref = data_ref.get("datasets") | 354 | datasets_ref = data_ref.get("datasets") |
| @@ -335,9 +363,46 @@ class ChartReviewService: | @@ -335,9 +363,46 @@ class ChartReviewService: | ||
| 335 | else: | 363 | else: |
| 336 | label_text = f"点{idx + 1}" | 364 | label_text = f"点{idx + 1}" |
| 337 | labels_from_data.append(str(label_text)) | 365 | labels_from_data.append(str(label_text)) |
| 366 | + | ||
| 338 | if labels_from_data: | 367 | if labels_from_data: |
| 339 | data_ref["labels"] = labels_from_data | 368 | data_ref["labels"] = labels_from_data |
| 340 | 369 | ||
| 370 | + @staticmethod | ||
| 371 | + def _is_chart_data_empty(data: Dict[str, Any] | None) -> bool: | ||
| 372 | + """检查图表数据是否为空或缺少有效 datasets""" | ||
| 373 | + if not isinstance(data, dict): | ||
| 374 | + return True | ||
| 375 | + | ||
| 376 | + datasets = data.get("datasets") | ||
| 377 | + if not isinstance(datasets, list) or len(datasets) == 0: | ||
| 378 | + return True | ||
| 379 | + | ||
| 380 | + for ds in datasets: | ||
| 381 | + if not isinstance(ds, dict): | ||
| 382 | + continue | ||
| 383 | + series = ds.get("data") | ||
| 384 | + if isinstance(series, list) and len(series) > 0: | ||
| 385 | + return False | ||
| 386 | + | ||
| 387 | + return True | ||
| 388 | + | ||
| 389 | + @staticmethod | ||
| 390 | + def _merge_dicts( | ||
| 391 | + base: Dict[str, Any] | None, override: Dict[str, Any] | None | ||
| 392 | + ) -> Dict[str, Any]: | ||
| 393 | + """ | ||
| 394 | + 递归合并两个字典,override 覆盖 base,均为新副本,避免副作用。 | ||
| 395 | + """ | ||
| 396 | + result = copy.deepcopy(base) if isinstance(base, dict) else {} | ||
| 397 | + if not isinstance(override, dict): | ||
| 398 | + return result | ||
| 399 | + for key, value in override.items(): | ||
| 400 | + if isinstance(value, dict) and isinstance(result.get(key), dict): | ||
| 401 | + result[key] = ChartReviewService._merge_dicts(result[key], value) | ||
| 402 | + else: | ||
| 403 | + result[key] = copy.deepcopy(value) | ||
| 404 | + return result | ||
| 405 | + | ||
| 341 | def _format_error_reason(self, validation_result: ValidationResult | None) -> str: | 406 | def _format_error_reason(self, validation_result: ValidationResult | None) -> str: |
| 342 | """格式化错误原因""" | 407 | """格式化错误原因""" |
| 343 | if not validation_result: | 408 | if not validation_result: |
-
Please register or login to post a comment