马一丁

Fixed the Error "AttributeError: 'list' object has no attribute 'get'"

@@ -39,6 +39,10 @@ from .state import ReportState @@ -39,6 +39,10 @@ from .state import ReportState
39 from .utils.config import settings, Settings 39 from .utils.config import settings, Settings
40 40
41 41
  42 +class StageOutputFormatError(ValueError):
  43 + """阶段性输出结构不符合预期时抛出的受控异常。"""
  44 +
  45 +
42 class FileCountBaseline: 46 class FileCountBaseline:
43 """ 47 """
44 文件数量基准管理器。 48 文件数量基准管理器。
@@ -177,6 +181,7 @@ class ReportAgent: @@ -177,6 +181,7 @@ class ReportAgent:
177 """ 181 """
178 _CONTENT_SPARSE_MIN_ATTEMPTS = 3 182 _CONTENT_SPARSE_MIN_ATTEMPTS = 3
179 _CONTENT_SPARSE_WARNING_TEXT = "本章LLM生成的内容字数可能过低,必要时可以尝试重新运行程序。" 183 _CONTENT_SPARSE_WARNING_TEXT = "本章LLM生成的内容字数可能过低,必要时可以尝试重新运行程序。"
  184 + _STRUCTURAL_RETRY_ATTEMPTS = 2
180 185
181 def __init__(self, config: Optional[Settings] = None): 186 def __init__(self, config: Optional[Settings] = None):
182 """ 187 """
@@ -421,6 +426,11 @@ class ReportAgent: @@ -421,6 +426,11 @@ class ReportAgent:
421 426
422 try: 427 try:
423 template_result = self._select_template(query, reports, forum_logs, custom_template) 428 template_result = self._select_template(query, reports, forum_logs, custom_template)
  429 + template_result = self._ensure_mapping(
  430 + template_result,
  431 + "模板选择结果",
  432 + expected_keys=["template_name", "template_content"],
  433 + )
424 self.state.metadata.template_used = template_result.get('template_name', '') 434 self.state.metadata.template_used = template_result.get('template_name', '')
425 emit('stage', { 435 emit('stage', {
426 'stage': 'template_selected', 436 'stage': 'template_selected',
@@ -436,13 +446,17 @@ class ReportAgent: @@ -436,13 +446,17 @@ class ReportAgent:
436 template_text = template_result.get('template_content', '') 446 template_text = template_result.get('template_content', '')
437 template_overview = self._build_template_overview(template_text, sections) 447 template_overview = self._build_template_overview(template_text, sections)
438 # 基于模板骨架+三引擎内容设计全局标题、目录与视觉主题 448 # 基于模板骨架+三引擎内容设计全局标题、目录与视觉主题
439 - layout_design = self.document_layout_node.run(  
440 - sections,  
441 - template_text,  
442 - normalized_reports,  
443 - forum_logs,  
444 - query,  
445 - template_overview, 449 + layout_design = self._run_stage_with_retry(
  450 + "文档设计",
  451 + lambda: self.document_layout_node.run(
  452 + sections,
  453 + template_text,
  454 + normalized_reports,
  455 + forum_logs,
  456 + query,
  457 + template_overview,
  458 + ),
  459 + expected_keys=["title", "hero", "toc", "tocPlan"],
446 ) 460 )
447 emit('stage', { 461 emit('stage', {
448 'stage': 'layout_designed', 462 'stage': 'layout_designed',
@@ -451,13 +465,18 @@ class ReportAgent: @@ -451,13 +465,18 @@ class ReportAgent:
451 }) 465 })
452 emit('progress', {'progress': 15, 'message': '文档标题/目录设计完成'}) 466 emit('progress', {'progress': 15, 'message': '文档标题/目录设计完成'})
453 # 使用刚生成的设计稿对全书进行篇幅规划,约束各章字数与重点 467 # 使用刚生成的设计稿对全书进行篇幅规划,约束各章字数与重点
454 - word_plan = self.word_budget_node.run(  
455 - sections,  
456 - layout_design,  
457 - normalized_reports,  
458 - forum_logs,  
459 - query,  
460 - template_overview, 468 + word_plan = self._run_stage_with_retry(
  469 + "章节篇幅规划",
  470 + lambda: self.word_budget_node.run(
  471 + sections,
  472 + layout_design,
  473 + normalized_reports,
  474 + forum_logs,
  475 + query,
  476 + template_overview,
  477 + ),
  478 + expected_keys=["chapters", "totalWords", "globalGuidelines"],
  479 + postprocess=self._normalize_word_plan,
461 ) 480 )
462 emit('stage', { 481 emit('stage', {
463 'stage': 'word_plan_ready', 482 'stage': 'word_plan_ready',
@@ -871,6 +890,134 @@ class ReportAgent: @@ -871,6 +890,134 @@ class ReportAgent:
871 ] 890 ]
872 return any(keyword in normalized for keyword in keywords) 891 return any(keyword in normalized for keyword in keywords)
873 892
  893 + def _run_stage_with_retry(
  894 + self,
  895 + stage_name: str,
  896 + fn: Callable[[], Any],
  897 + expected_keys: Optional[List[str]] = None,
  898 + postprocess: Optional[Callable[[Dict[str, Any], str], Dict[str, Any]]] = None,
  899 + ) -> Dict[str, Any]:
  900 + """
  901 + 运行单个LLM阶段并在结构异常时有限次重试。
  902 +
  903 + 该方法只针对结构类错误做本地修复/重试,避免整个Agent重启。
  904 + """
  905 + last_error: Optional[Exception] = None
  906 + for attempt in range(1, self._STRUCTURAL_RETRY_ATTEMPTS + 1):
  907 + try:
  908 + raw_result = fn()
  909 + result = self._ensure_mapping(raw_result, stage_name, expected_keys)
  910 + if postprocess:
  911 + result = postprocess(result, stage_name)
  912 + return result
  913 + except StageOutputFormatError as exc:
  914 + last_error = exc
  915 + logger.warning(
  916 + "{stage} 输出结构异常(第 {attempt}/{total} 次),将尝试修复或重试: {error}",
  917 + stage=stage_name,
  918 + attempt=attempt,
  919 + total=self._STRUCTURAL_RETRY_ATTEMPTS,
  920 + error=exc,
  921 + )
  922 + if attempt >= self._STRUCTURAL_RETRY_ATTEMPTS:
  923 + break
  924 + raise last_error # type: ignore[misc]
  925 +
  926 + def _ensure_mapping(
  927 + self,
  928 + value: Any,
  929 + context: str,
  930 + expected_keys: Optional[List[str]] = None,
  931 + ) -> Dict[str, Any]:
  932 + """
  933 + 确保阶段输出为dict;若返回列表则尝试提取最佳匹配元素。
  934 + """
  935 + if isinstance(value, dict):
  936 + return value
  937 +
  938 + if isinstance(value, list):
  939 + candidates = [item for item in value if isinstance(item, dict)]
  940 + if candidates:
  941 + best = candidates[0]
  942 + if expected_keys:
  943 + candidates.sort(
  944 + key=lambda item: sum(1 for key in expected_keys if key in item),
  945 + reverse=True,
  946 + )
  947 + best = candidates[0]
  948 + logger.warning(
  949 + "{context} 返回列表,已自动提取包含最多预期键的元素继续执行",
  950 + context=context,
  951 + )
  952 + return best
  953 + raise StageOutputFormatError(f"{context} 返回列表但缺少可用的对象元素")
  954 +
  955 + if value is None:
  956 + raise StageOutputFormatError(f"{context} 返回空结果")
  957 +
  958 + raise StageOutputFormatError(
  959 + f"{context} 返回类型 {type(value).__name__},期望字典"
  960 + )
  961 +
  962 + def _normalize_word_plan(self, word_plan: Dict[str, Any], stage_name: str) -> Dict[str, Any]:
  963 + """
  964 + 清洗篇幅规划结果,确保 chapters/globalGuidelines/totalWords 类型安全。
  965 + """
  966 + raw_chapters = word_plan.get("chapters", [])
  967 + if isinstance(raw_chapters, dict):
  968 + chapters_iterable = raw_chapters.values()
  969 + elif isinstance(raw_chapters, list):
  970 + chapters_iterable = raw_chapters
  971 + else:
  972 + chapters_iterable = []
  973 +
  974 + normalized: List[Dict[str, Any]] = []
  975 + for idx, entry in enumerate(chapters_iterable):
  976 + if isinstance(entry, dict):
  977 + normalized.append(entry)
  978 + continue
  979 + if isinstance(entry, list):
  980 + dict_candidate = next((item for item in entry if isinstance(item, dict)), None)
  981 + if dict_candidate:
  982 + logger.warning(
  983 + "{stage} 第 {idx} 个章节条目为列表,已提取首个对象用于后续流程",
  984 + stage=stage_name,
  985 + idx=idx + 1,
  986 + )
  987 + normalized.append(dict_candidate)
  988 + continue
  989 + logger.warning(
  990 + "{stage} 跳过无法解析的章节条目#{idx}(类型: {type_name})",
  991 + stage=stage_name,
  992 + idx=idx + 1,
  993 + type_name=type(entry).__name__,
  994 + )
  995 +
  996 + if not normalized:
  997 + raise StageOutputFormatError(f"{stage_name} 缺少有效的章节规划,无法继续")
  998 +
  999 + word_plan["chapters"] = normalized
  1000 +
  1001 + guidelines = word_plan.get("globalGuidelines")
  1002 + if not isinstance(guidelines, list):
  1003 + if guidelines is None or guidelines == "":
  1004 + word_plan["globalGuidelines"] = []
  1005 + else:
  1006 + logger.warning(
  1007 + "{stage} globalGuidelines 类型异常,已转换为列表封装",
  1008 + stage=stage_name,
  1009 + )
  1010 + word_plan["globalGuidelines"] = [guidelines]
  1011 +
  1012 + if not isinstance(word_plan.get("totalWords"), (int, float)):
  1013 + logger.warning(
  1014 + "{stage} totalWords 类型异常,使用默认值 10000",
  1015 + stage=stage_name,
  1016 + )
  1017 + word_plan["totalWords"] = 10000
  1018 +
  1019 + return word_plan
  1020 +
874 def _finalize_sparse_chapter(self, chapter: Optional[Dict[str, Any]]) -> Dict[str, Any]: 1021 def _finalize_sparse_chapter(self, chapter: Optional[Dict[str, Any]]) -> Dict[str, Any]:
875 """ 1022 """
876 构造内容稀疏兜底章节:复制原始payload并插入温馨提示段落。 1023 构造内容稀疏兜底章节:复制原始payload并插入温馨提示段落。
@@ -584,6 +584,9 @@ def generate_report(): @@ -584,6 +584,9 @@ def generate_report():
584 584
585 # 获取请求参数 585 # 获取请求参数
586 data = request.get_json() or {} 586 data = request.get_json() or {}
  587 + if not isinstance(data, dict):
  588 + logger.warning("generate_report 接收到非对象JSON负载,已忽略原始内容")
  589 + data = {}
587 query = data.get('query', '智能舆情分析报告') 590 query = data.get('query', '智能舆情分析报告')
588 custom_template = data.get('custom_template', '') 591 custom_template = data.get('custom_template', '')
589 592
@@ -1285,7 +1288,13 @@ def export_pdf_from_ir(): @@ -1285,7 +1288,13 @@ def export_pdf_from_ir():
1285 'system_message': pango_message 1288 'system_message': pango_message
1286 }), 503 1289 }), 503
1287 1290
1288 - data = request.get_json() 1291 + data = request.get_json() or {}
  1292 + if not isinstance(data, dict):
  1293 + logger.warning("export_pdf_from_ir 请求体不是JSON对象")
  1294 + return jsonify({
  1295 + 'success': False,
  1296 + 'error': '请求体必须是JSON对象'
  1297 + }), 400
1289 1298
1290 if not data or 'document_ir' not in data: 1299 if not data or 'document_ir' not in data:
1291 return jsonify({ 1300 return jsonify({