Doiiars

1. 修复论坛通信问题

2. 修复总结报告错误
3. 修复环境变量重新载入问题
4. 添加测试用例
5. 修复论坛主持人问题
@@ -12,7 +12,7 @@ import re @@ -12,7 +12,7 @@ import re
12 12
13 # 添加项目根目录到Python路径以导入config 13 # 添加项目根目录到Python路径以导入config
14 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 14 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15 -from config import FORUM_HOST_API_KEY, FORUM_HOST_BASE_URL, FORUM_HOST_MODEL_NAME 15 +from config import settings
16 16
17 # 添加utils目录到Python路径 17 # 添加utils目录到Python路径
18 current_dir = os.path.dirname(os.path.abspath(__file__)) 18 current_dir = os.path.dirname(os.path.abspath(__file__))
@@ -35,21 +35,21 @@ class ForumHost: @@ -35,21 +35,21 @@ class ForumHost:
35 初始化论坛主持人 35 初始化论坛主持人
36 36
37 Args: 37 Args:
38 - api_key: 硅基流动API密钥,如果不提供则从配置文件读取  
39 - base_url: 接口基础地址,默认使用配置文件提供的SiliconFlow地址 38 + api_key: 论坛主持人 LLM API 密钥,如果不提供则从配置文件读取
  39 + base_url: 论坛主持人 LLM API 接口基础地址,默认使用配置文件提供的SiliconFlow地址
40 """ 40 """
41 - self.api_key = api_key or FORUM_HOST_API_KEY 41 + self.api_key = api_key or settings.FORUM_HOST_API_KEY
42 42
43 if not self.api_key: 43 if not self.api_key:
44 - raise ValueError("未找到硅基流动API密钥,请在config.py中设置FORUM_HOST_API_KEY") 44 + raise ValueError("未找到论坛主持人API密钥,请在环境变量文件中设置FORUM_HOST_API_KEY")
45 45
46 - self.base_url = base_url or FORUM_HOST_BASE_URL 46 + self.base_url = base_url or settings.FORUM_HOST_BASE_URL
47 47
48 self.client = OpenAI( 48 self.client = OpenAI(
49 api_key=self.api_key, 49 api_key=self.api_key,
50 base_url=self.base_url 50 base_url=self.base_url
51 ) 51 )
52 - self.model = model_name or FORUM_HOST_MODEL_NAME # Use configured model 52 + self.model = model_name or settings.FORUM_HOST_MODEL_NAME # Use configured model
53 53
54 # Track previous summaries to avoid duplicates 54 # Track previous summaries to avoid duplicates
55 self.previous_summaries = [] 55 self.previous_summaries = []
@@ -18,7 +18,7 @@ try: @@ -18,7 +18,7 @@ try:
18 from .llm_host import generate_host_speech 18 from .llm_host import generate_host_speech
19 HOST_AVAILABLE = True 19 HOST_AVAILABLE = True
20 except ImportError: 20 except ImportError:
21 - logger.warning("ForumEngine: 论坛主持人模块未找到,将以纯监控模式运行") 21 + logger.exception("ForumEngine: 论坛主持人模块未找到,将以纯监控模式运行")
22 HOST_AVAILABLE = False 22 HOST_AVAILABLE = False
23 23
24 class LogMonitor: 24 class LogMonitor:
@@ -50,10 +50,20 @@ class LogMonitor: @@ -50,10 +50,20 @@ class LogMonitor:
50 self.host_speech_threshold = 5 # 每5条agent发言触发一次主持人发言 50 self.host_speech_threshold = 5 # 每5条agent发言触发一次主持人发言
51 self.is_host_generating = False # 主持人是否正在生成发言 51 self.is_host_generating = False # 主持人是否正在生成发言
52 52
53 - # 目标节点名称 - 直接匹配字符串  
54 - self.target_nodes = [  
55 - 'FirstSummaryNode',  
56 - 'ReflectionSummaryNode' 53 + # 目标节点识别模式
  54 + # 1. 类名(旧格式可能包含)
  55 + # 2. 完整模块路径(实际日志格式,包含引擎前缀)
  56 + # 3. 部分模块路径(兼容性)
  57 + # 4. 关键标识文本
  58 + self.target_node_patterns = [
  59 + 'FirstSummaryNode', # 类名
  60 + 'ReflectionSummaryNode', # 类名
  61 + 'InsightEngine.nodes.summary_node', # InsightEngine完整路径
  62 + 'MediaEngine.nodes.summary_node', # MediaEngine完整路径
  63 + 'QueryEngine.nodes.summary_node', # QueryEngine完整路径
  64 + 'nodes.summary_node', # 模块路径(兼容性,用于部分匹配)
  65 + '正在生成首次段落总结', # FirstSummaryNode的标识
  66 + '正在生成反思总结', # ReflectionSummaryNode的标识
57 ] 67 ]
58 68
59 # 多行内容捕获状态 69 # 多行内容捕获状态
@@ -107,12 +117,33 @@ class LogMonitor: @@ -107,12 +117,33 @@ class LogMonitor:
107 f.flush() 117 f.flush()
108 except Exception as e: 118 except Exception as e:
109 logger.exception(f"ForumEngine: 写入forum.log失败: {e}") 119 logger.exception(f"ForumEngine: 写入forum.log失败: {e}")
110 - 120 +
111 def is_target_log_line(self, line: str) -> bool: 121 def is_target_log_line(self, line: str) -> bool:
112 - """检查是否是目标日志行(SummaryNode)"""  
113 - # 简单字符串包含检查,更可靠  
114 - for node_name in self.target_nodes:  
115 - if node_name in line: 122 + """检查是否是目标日志行(SummaryNode)
  123 +
  124 + 支持多种识别方式:
  125 + 1. 类名:FirstSummaryNode, ReflectionSummaryNode
  126 + 2. 完整模块路径:InsightEngine.nodes.summary_node、MediaEngine.nodes.summary_node、QueryEngine.nodes.summary_node
  127 + 3. 部分模块路径:nodes.summary_node(兼容性)
  128 + 4. 关键标识文本:正在生成首次段落总结、正在生成反思总结
  129 +
  130 + 排除条件:
  131 + - ERROR 级别的日志(错误日志不应被识别为目标节点)
  132 + - 包含错误关键词的日志(JSON解析失败、JSON修复失败等)
  133 + """
  134 + # 排除 ERROR 级别的日志
  135 + if "| ERROR" in line or "| ERROR |" in line:
  136 + return False
  137 +
  138 + # 排除包含错误关键词的日志
  139 + error_keywords = ["JSON解析失败", "JSON修复失败", "Traceback", "File \""]
  140 + for keyword in error_keywords:
  141 + if keyword in line:
  142 + return False
  143 +
  144 + # 检查是否包含目标节点模式
  145 + for pattern in self.target_node_patterns:
  146 + if pattern in line:
116 return True 147 return True
117 return False 148 return False
118 149
@@ -381,32 +412,33 @@ class LogMonitor: @@ -381,32 +412,33 @@ class LogMonitor:
381 if not line.strip(): 412 if not line.strip():
382 continue 413 continue
383 414
384 - # 检查是否是目标节点行或包含JSON开始标记的行 415 + # 检查是否是目标节点行和JSON开始标记
385 is_target = self.is_target_log_line(line) 416 is_target = self.is_target_log_line(line)
386 is_json_start = self.is_json_start_line(line) 417 is_json_start = self.is_json_start_line(line)
387 418
388 - if is_target or is_json_start:  
389 - if is_json_start:  
390 - # 开始捕获JSON(即使不是目标节点,只要包含"清理后的输出: {"就处理)  
391 - self.capturing_json[app_name] = True  
392 - self.json_buffer[app_name] = [line]  
393 - self.json_start_line[app_name] = line 419 + # 只有目标节点(SummaryNode)的JSON输出才应该被捕获
  420 + # 过滤掉SearchNode等其他节点的输出(它们不是目标节点,即使有JSON也不会被捕获)
  421 + if is_target and is_json_start:
  422 + # 开始捕获JSON(必须是目标节点且包含"清理后的输出: {")
  423 + self.capturing_json[app_name] = True
  424 + self.json_buffer[app_name] = [line]
  425 + self.json_start_line[app_name] = line
  426 +
  427 + # 检查是否是单行JSON
  428 + if line.strip().endswith("}"):
  429 + # 单行JSON,立即处理
  430 + content = self.extract_json_content([line])
  431 + if content: # 只有成功解析的内容才会被记录
  432 + # 去除重复的标签和格式化
  433 + clean_content = self._clean_content_tags(content, app_name)
  434 + captured_contents.append(f"{clean_content}")
  435 + self.capturing_json[app_name] = False
  436 + self.json_buffer[app_name] = []
394 437
395 - # 检查是否是单行JSON  
396 - if line.strip().endswith("}"):  
397 - # 单行JSON,立即处理  
398 - content = self.extract_json_content([line])  
399 - if content: # 只有成功解析的内容才会被记录  
400 - # 去除重复的标签和格式化  
401 - clean_content = self._clean_content_tags(content, app_name)  
402 - captured_contents.append(f"{clean_content}")  
403 - self.capturing_json[app_name] = False  
404 - self.json_buffer[app_name] = []  
405 -  
406 - elif is_target and self.is_valuable_content(line):  
407 - # 其他有价值的SummaryNode内容(必须是目标节点且有价值)  
408 - clean_content = self._clean_content_tags(self.extract_node_content(line), app_name)  
409 - captured_contents.append(f"{clean_content}") 438 + elif is_target and self.is_valuable_content(line):
  439 + # 其他有价值的SummaryNode内容(必须是目标节点且有价值)
  440 + clean_content = self._clean_content_tags(self.extract_node_content(line), app_name)
  441 + captured_contents.append(f"{clean_content}")
410 442
411 elif self.capturing_json[app_name]: 443 elif self.capturing_json[app_name]:
412 # 正在捕获JSON的后续行 444 # 正在捕获JSON的后续行
@@ -528,13 +560,16 @@ class LogMonitor: @@ -528,13 +560,16 @@ class LogMonitor:
528 # 先检查是否需要触发搜索(只触发一次) 560 # 先检查是否需要触发搜索(只触发一次)
529 if not self.is_searching: 561 if not self.is_searching:
530 for line in new_lines: 562 for line in new_lines:
531 - if line.strip() and 'FirstSummaryNode' in line:  
532 - logger.info(f"ForumEngine: 在{app_name}中检测到第一次论坛发表内容")  
533 - self.is_searching = True  
534 - self.search_inactive_count = 0  
535 - # 清空forum.log开始新会话  
536 - self.clear_forum_log()  
537 - break # 找到一个就够了,跳出循环 563 + # 检查是否包含目标节点模式(支持多种格式)
  564 + if line.strip() and self.is_target_log_line(line):
  565 + # 进一步确认是首次总结节点(FirstSummaryNode或包含"正在生成首次段落总结")
  566 + if 'FirstSummaryNode' in line or '正在生成首次段落总结' in line:
  567 + logger.info(f"ForumEngine: 在{app_name}中检测到第一次论坛发表内容")
  568 + self.is_searching = True
  569 + self.search_inactive_count = 0
  570 + # 清空forum.log开始新会话
  571 + self.clear_forum_log()
  572 + break # 找到一个就够了,跳出循环
538 573
539 # 处理所有新增内容(如果正在搜索状态) 574 # 处理所有新增内容(如果正在搜索状态)
540 if self.is_searching: 575 if self.is_searching:
@@ -161,6 +161,11 @@ class KeywordOptimizer: @@ -161,6 +161,11 @@ class KeywordOptimizer:
161 161
162 **重要提醒**:每个关键词都必须是一个不可分割的独立词条,严禁在词条内部包含空格。例如,应使用 "雷军班争议" 而不是错误的 "雷军班 争议"。 162 **重要提醒**:每个关键词都必须是一个不可分割的独立词条,严禁在词条内部包含空格。例如,应使用 "雷军班争议" 而不是错误的 "雷军班 争议"。
163 163
  164 +**主题相关**:关键词要与初始查询主题相关,不要偏离主题
  165 + - 保留主体词("武汉天气"→"武汉"✓)
  166 + - 避免泛化属性("天气"✗ 会匹配全国)
  167 + - 专有名词可拆("武汉大学"→"武大"、"大学"✓)
  168 +
164 **输出格式**: 169 **输出格式**:
165 请以JSON格式返回结果: 170 请以JSON格式返回结果:
166 { 171 {
@@ -195,7 +195,7 @@ class ReportAgent: @@ -195,7 +195,7 @@ class ReportAgent:
195 start_time = datetime.now() 195 start_time = datetime.now()
196 196
197 logger.info(f"开始生成报告: {query}") 197 logger.info(f"开始生成报告: {query}")
198 - self.logger.info(f"输入数据 - 报告数量: {len(reports)}, 论坛日志长度: {len(forum_logs)}") 198 + logger.info(f"输入数据 - 报告数量: {len(reports)}, 论坛日志长度: {len(forum_logs)}")
199 199
200 try: 200 try:
201 # Step 1: 模板选择 201 # Step 1: 模板选择
@@ -134,6 +134,7 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = " @@ -134,6 +134,7 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = "
134 task.update_status("completed", 100) 134 task.update_status("completed", 100)
135 135
136 except Exception as e: 136 except Exception as e:
  137 + logger.exception(f"报告生成过程中发生错误: {str(e)}")
137 task.update_status("error", 0, str(e)) 138 task.update_status("error", 0, str(e))
138 # 只在出错时清理任务 139 # 只在出错时清理任务
139 with task_lock: 140 with task_lock:
@@ -156,6 +157,7 @@ def get_status(): @@ -156,6 +157,7 @@ def get_status():
156 'current_task': current_task.to_dict() if current_task else None 157 'current_task': current_task.to_dict() if current_task else None
157 }) 158 })
158 except Exception as e: 159 except Exception as e:
  160 + logger.exception(f"获取Report Engine状态失败: {str(e)}")
159 return jsonify({ 161 return jsonify({
160 'success': False, 162 'success': False,
161 'error': str(e) 163 'error': str(e)
@@ -228,6 +230,7 @@ def generate_report(): @@ -228,6 +230,7 @@ def generate_report():
228 }) 230 })
229 231
230 except Exception as e: 232 except Exception as e:
  233 + logger.exception(f"开始生成报告失败: {str(e)}")
231 return jsonify({ 234 return jsonify({
232 'success': False, 235 'success': False,
233 'error': str(e) 236 'error': str(e)
@@ -319,6 +322,7 @@ def get_result_json(task_id: str): @@ -319,6 +322,7 @@ def get_result_json(task_id: str):
319 }) 322 })
320 323
321 except Exception as e: 324 except Exception as e:
  325 + logger.exception(f"获取报告生成结果失败: {str(e)}")
322 return jsonify({ 326 return jsonify({
323 'success': False, 327 'success': False,
324 'error': str(e) 328 'error': str(e)
@@ -348,6 +352,7 @@ def cancel_task(task_id: str): @@ -348,6 +352,7 @@ def cancel_task(task_id: str):
348 }), 404 352 }), 404
349 353
350 except Exception as e: 354 except Exception as e:
  355 + logger.exception(f"取消报告生成任务失败: {str(e)}")
351 return jsonify({ 356 return jsonify({
352 'success': False, 357 'success': False,
353 'error': str(e) 358 'error': str(e)
@@ -391,6 +396,7 @@ def get_templates(): @@ -391,6 +396,7 @@ def get_templates():
391 }) 396 })
392 397
393 except Exception as e: 398 except Exception as e:
  399 + logger.exception(f"获取可用模板列表失败: {str(e)}")
394 return jsonify({ 400 return jsonify({
395 'success': False, 401 'success': False,
396 'error': str(e) 402 'error': str(e)
@@ -400,6 +406,7 @@ def get_templates(): @@ -400,6 +406,7 @@ def get_templates():
400 # 错误处理 406 # 错误处理
401 @report_bp.errorhandler(404) 407 @report_bp.errorhandler(404)
402 def not_found(error): 408 def not_found(error):
  409 + logger.exception(f"API端点不存在: {str(error)}")
403 return jsonify({ 410 return jsonify({
404 'success': False, 411 'success': False,
405 'error': 'API端点不存在' 412 'error': 'API端点不存在'
@@ -408,6 +415,7 @@ def not_found(error): @@ -408,6 +415,7 @@ def not_found(error):
408 415
409 @report_bp.errorhandler(500) 416 @report_bp.errorhandler(500)
410 def internal_error(error): 417 def internal_error(error):
  418 + logger.exception(f"服务器内部错误: {str(error)}")
411 return jsonify({ 419 return jsonify({
412 'success': False, 420 'success': False,
413 'error': '服务器内部错误' 421 'error': '服务器内部错误'
@@ -94,20 +94,10 @@ def _load_config_module(): @@ -94,20 +94,10 @@ def _load_config_module():
94 def read_config_values(): 94 def read_config_values():
95 """Return the current configuration values that are exposed to the frontend.""" 95 """Return the current configuration values that are exposed to the frontend."""
96 try: 96 try:
97 - # 重新导入 config 模块以获取最新的 Settings 实例  
98 - importlib.invalidate_caches()  
99 - if CONFIG_MODULE_NAME in sys.modules:  
100 - importlib.reload(sys.modules[CONFIG_MODULE_NAME])  
101 - else:  
102 - importlib.import_module(CONFIG_MODULE_NAME)  
103 -  
104 - # 从 config 模块获取 settings 实例  
105 - config_module = sys.modules[CONFIG_MODULE_NAME]  
106 - if not hasattr(config_module, 'settings'):  
107 - logger.error("config 模块中没有找到 settings 实例")  
108 - return {} 97 + # 重新加载配置以获取最新的 Settings 实例
  98 + from config import reload_settings, settings
  99 + reload_settings()
109 100
110 - settings = config_module.settings  
111 values = {} 101 values = {}
112 for key in CONFIG_KEYS: 102 for key in CONFIG_KEYS:
113 # 从 Pydantic Settings 实例读取值 103 # 从 Pydantic Settings 实例读取值
@@ -9,8 +9,9 @@ @@ -9,8 +9,9 @@
9 9
10 from pathlib import Path 10 from pathlib import Path
11 from pydantic_settings import BaseSettings 11 from pydantic_settings import BaseSettings
12 -from pydantic import Field 12 +from pydantic import Field, ConfigDict
13 from typing import Optional 13 from typing import Optional
  14 +from loguru import logger
14 15
15 16
16 # 计算 .env 优先级:优先当前工作目录,其次项目根目录 17 # 计算 .env 优先级:优先当前工作目录,其次项目根目录
@@ -86,12 +87,29 @@ class Settings(BaseSettings): @@ -86,12 +87,29 @@ class Settings(BaseSettings):
86 SEARCH_TIMEOUT: int = Field(240, description="单次搜索请求超时") 87 SEARCH_TIMEOUT: int = Field(240, description="单次搜索请求超时")
87 MAX_CONTENT_LENGTH: int = Field(500000, description="搜索最大内容长度") 88 MAX_CONTENT_LENGTH: int = Field(500000, description="搜索最大内容长度")
88 89
89 - class Config:  
90 - env_file = ENV_FILE  
91 - env_prefix = ""  
92 - case_sensitive = False  
93 - extra = "allow" 90 + model_config = ConfigDict(
  91 + env_file=ENV_FILE,
  92 + env_prefix="",
  93 + case_sensitive=False,
  94 + extra="allow"
  95 + )
94 96
95 97
96 # 创建全局配置实例 98 # 创建全局配置实例
97 settings = Settings() 99 settings = Settings()
  100 +
  101 +
  102 +def reload_settings() -> Settings:
  103 + """
  104 + 重新加载配置
  105 +
  106 + 从 .env 文件和环境变量重新加载配置,更新全局 settings 实例。
  107 + 用于在运行时动态更新配置。
  108 +
  109 + Returns:
  110 + Settings: 新创建的配置实例
  111 + """
  112 +
  113 + global settings
  114 + settings = Settings()
  115 + return settings
@@ -104,3 +104,54 @@ MIXED_FORMAT_LINES = [ @@ -104,3 +104,54 @@ MIXED_FORMAT_LINES = [
104 "[17:42:31] }" 104 "[17:42:31] }"
105 ] 105 ]
106 106
  107 +# ===== 实际生产环境日志示例 =====
  108 +
  109 +# QueryEngine反思总结 - 多行JSON格式
  110 +REAL_QUERY_ENGINE_REFLECTION = [
  111 + "[10:56:04] 2025-11-06 10:56:04.759 | INFO | QueryEngine.nodes.summary_node:process_output:302 - 清理后的输出: {",
  112 + "[10:56:04] \"updated_paragraph_latest_state\": \"洛阳栾川钼业集团股份有限公司(简称洛阳钼业,CMOC)是中国大陆领先的钼生产企业,同时也是全球顶级的有色金属和稀有金属生产商之一。公司前身可追溯至1969年经原国家冶金部审批成立的栾川县小型选矿厂。1999年,洛阳栾川钼业集团正式成立,并于2006年改制为股份制有限公司。历经2004年和2014年的两次混合所有制改革,洛阳钼业目前是一家民营控股的股份制公司。公司于2007年在香港联合交易所上市(股票代码:03993),并于2012年回归A股在上海证券交易所上市(股票代码:603993)。\\n\\n洛阳钼业的核心业务涵盖基本金属、稀有金属的采、选、冶及加工,包括钼、钨、铜、钴、铌、磷等,同时积极开展矿产贸易业务。公司的业务足迹遍布亚洲、非洲、南美洲和欧洲,是全球领先的铜、钴、钼、钨、铌生产商,也是巴西领先的磷肥生产商。截至2025年三季度,公司实现营业收入1454.85亿元,归母净利润86.71亿元,经营性净现金流达120.09亿元,资产负债率进一步优化至50.15%\\n\\n### 战略与运营升级\\n2025年公司迎来重要战略转折,引入具有国际化视野的管理团队,新董事长刘建锋(前中国海油集团商务总监)与常务副总裁阙朝阳(前紫金矿业集团高管)主导推动组织架构革新。通过收购厄瓜多尔Cangrejos金矿(预计2029年投产)等关键并购,实现资产组合向多品种(铜、黄金为主)、多国家(覆盖非洲、南美)、多阶段(生产与绿地项目结合)转型。刚果(金)核心矿区通过『小改小革』持续优化,2028年规划铜产能达80-100万吨,NziloII水电站建设有效缓解能源约束。\\n\\n### 行业地位与财务表现\\n据2025年《财富》中国500强最新排名,洛阳钼业以2130.29亿元营收跃居第138位,市值突破2500亿元。公司独创『矿山+贸易』双轮模式,IXM贸易平台掌控全球12%铜精矿贸易量,TFM与KFM项目成本分别进入全球前30%和前10%分位。新能源金属领域保持绝对优势,钴产能占全球40%以上,近期刚果(金)出口管制政策促使钴价反弹,2025年上半年均价较2024年提升26%\\n\\n### 技术与社会责任\\n5G智慧矿山实现穿孔、运输、破碎全流程无人化,开采成本较国际同行低18-22%。ESG建设方面,MSCI评级提升至BBB级,通过TCFD框架披露气候风险,矿山电动化率35%,减排强度同比下降19%。2025年实施每股0.255元现金分红,持续回馈股东。\\n\\n### 未来展望\\n管理层预计2026-2028年将维持高强度并购,重点关注铜金资源,已储备多个潜在项目。厄瓜多尔Cangrejos金矿(预计年产黄金11.5吨)与KFM二期扩建构成中期增长极,摩根士丹利预测2027年铜当量产能突破150万吨。公司资产负债率控制在50%安全线内,在手现金超320亿元,为战略布局提供充足弹药。\"",
  113 + "[10:56:04] }"
  114 +]
  115 +
  116 +# InsightEngine反思总结 - 多行JSON格式(包含"正在生成反思总结"标识)
  117 +REAL_INSIGHT_ENGINE_REFLECTION = [
  118 + "[10:55:19] 2025-11-06 10:55:19.563 | INFO | InsightEngine.nodes.summary_node:run:265 - 正在生成反思总结",
  119 + "[10:56:41] 2025-11-06 10:56:41.626 | INFO | InsightEngine.nodes.summary_node:process_output:296 - 清理后的输出: {",
  120 + "[10:56:41] \"updated_paragraph_latest_state\": \"## 核心发现(更新版)\\n洛阳钼业2025年第三季度市场表现呈现结构性分化,在全球铜价同比上涨18%(LME三个月期铜均价$8,927/吨)的背景下,公司股价却累计下跌12.3%,与申万有色金属指数7.8%的涨幅形成鲜明对比。深入分析显示,这种背离主要源于三大矛盾:全球能源转型红利与区域性运营风险的博弈、资源禀赋优势与ESG短板的冲突、以及机构估值框架转变与散户认知滞后的错位。最新舆情监测发现,专业投资者讨论焦点已从产量数据转向刚果(金)社区索赔案件的司法进展(涉及金额预估2.3亿美元),而散户仍在热议『新能源金属』概念炒作。\\n\\n## 详细数据画像\\n### 产量与成本\\n- 刚果(金)TFM铜钴矿:Q3铜产量12.8万吨(环比-7%),钴产量5,200吨(环比-9%),单位现金成本升至$1.52/lb(Q2为$1.38),因当地罢工导致14天停产(损失产值约3.2亿元)\\n- 巴西铌磷矿:铌铁产量2.1万吨(同比+4%),磷肥产量28万吨(创纪录),海运费用占比升至23%(2024年平均17%),但因巴西雷亚尔贬值节约本地成本1.8亿元\\n- 澳洲NPM铜金矿:铜品位下滑至0.72%(上年同期0.81%),但通过提高回收率维持产量稳定(回收率提升2.3个百分点至89.7%\\n\\n### 财务指标\\n- 营收:Q3实现287亿元(同比+9.2%,环比-5.3%),低于彭博一致预期6%,主因铜钴销量下滑\\n- 现金流:经营活动现金流净额42亿元(同比-18%),资本开支达35亿元(KFM项目占72%\\n- 负债:资产负债率升至58.3%(2024年末54.1%),新增20亿元公司债票面利率6.8%(较同行高120bp)\\n\\n### 市场反应\\n- 股价表现:三季度累计换手率287%,显著高于紫金矿业(189%)和江西铜业(156%),振幅达43%\\n- 机构动向:北向资金持仓减少1.2亿股,挪威养老基金持股比例从2.1%降至1.4%(ESG调仓)\\n- 舆情热度:百度指数『洛阳钼业』日均搜索量3,215次(同业排名第4),但专业平台Wind词频统计显示分析师关注度排名第2(含327份研报)\\n\\n## 多元声音汇聚\\n产业视角:\\n1. 【Fastmarkets分析师】『刚果(金)新矿业税实施后,TFM项目有效税率从31.5%升至35.8%,每磅铜的税负增加$0.12』(报告被引用87次)\\n2. 【巴西矿业协会】『尽管海运成本上升,洛阳钼业的铌磷矿仍是全球成本曲线左端20%的优质资产』\\n3. 【刚果矿业部长声明】『要求外资矿业企业本地采购比例需在2026年前达到40%』(现行25%\\n4. 【澳洲矿产委员会】『NPM矿的劳工成本已超出可承受范围,可能影响2026年扩产计划』\\n\\n投资者声音:\\n5. 【雪球用户@价值挖掘机】『DCF模型显示,若刚果政策风险溢价上调200bp,公司合理估值应下调15-20%』(附详细测算表格,获专业认证)\\n6. 【股吧热帖】『社保基金三季报减持后,融资余额反而增加4.3亿元,多空博弈激烈』(单日点击量超10万)\\n7. 【推特机构账号】『MSCI将公司治理(G)评分从6.2降至5.4,主因董事会独立性问题』\\n8. 【机构投资者调研纪要】『至少7家基金质疑刚果子公司分红政策(近三年分红率仅12%)』\\n9. 【Reddit散户讨论】『看涨期权持仓量暴增300%,集中行权价9元』\\n\\n国际视角:\\n10. 【彭博社报道】『洛阳钼业与嘉能可的KFM项目股权谈判陷入僵局,双方对2026年后钴价预期差异达$5/lb』\\n11. 【刚果当地媒体】『TFM周边社区新提起3起环境诉讼,要求赔偿金合计8,000万美元』\\n12. 【澳洲矿业工人论坛】『NPM矿区的工会正酝酿新一轮薪资谈判,现有合同溢价已达行业平均125%\\n13. 【路透社】『中国进出口银行可能为KFM项目提供15亿美元再融资』\\n14. 【非洲发展银行报告】『刚果矿业社区冲突事件同比增加47%\\n\\n## 深层洞察升级\\n### 政策风险量化\\n通过蒙特卡洛模拟测算,在以下情境下:(1)刚果 royalty rate 上调3个百分点(2)海运成本维持当前水平(3)钴价徘徊在$25/lb,公司2026年EBITDA可能缩水23-28亿元。敏感性分析显示,刚果政策变量对估值影响权重从去年的18%升至31%。地缘政治专家指出,刚果大选临近使矿业政策不确定性指数达78(警戒线70)。\\n\\n### ESG影响拆解\\n- 环境(E):尾矿库管理被MSCI标红,主要因刚果项目的水循环利用率仅72%(国际同行平均85%),且2025年发生2次小规模渗漏\\n- 社会(S):社区关系评分暴跌,源于Q3当地雇佣比例降至43%(承诺目标60%),且医疗投入同比减少15%\\n- 治理(G):董事会中独立董事占比33%(仅1名具国际矿业经验),低于国际矿业公司平均45%的水平\\n\\n### 资金行为解析\\n龙虎榜数据显示,三季度机构专用席位净卖出23亿元(创历史季度纪录),但同时量化基金交易占比从12%升至19%,显示算法交易对股价波动增强效应。北向资金持仓成本分析表明,外资止损线集中在6.8元附近(现价7.2元)。值得注意的是,大宗交易溢价率从Q2的-3%收窄至-1.2%,暗示部分长线资金开始逢低吸纳。\\n\\n## 趋势和模式识别\\n1. 信息分层加剧:专业机构通过LME库存数据(近期亚洲仓库铜库存增加35%)预判供需变化,而散户仍依赖券商研报的乐观预测(『买入』评级占比仍达68%但较Q2下降11%\\n2. ESG因子定价权提升:负面评级直接导致11月3日股价跳空低开3.2%,创三个月最大单日缺口\\n3. 多空博弈新特征:融券余额历史首次突破5亿元(日均利率达8.6%),同时场外期权隐含波动率升至52%(高于行业平均38%\\n4. 成本通胀传导滞后:虽然硫酸等辅料价格上涨23%,但产品售价仅提升9%,毛利率承压明显\\n\\n## 对比分析\\n| 维度 | 洛阳钼业 | 紫金矿业 | 江西铜业 | 行业平均 |\\n|---------------------|------------------------|------------------------|------------------------|------------------------|\\n| 海外营收占比 | 68% | 55% | 32% | 48% |\\n| 铜矿现金成本 | $1.52/lb | $1.35/lb | $1.48/lb | $1.45/lb |\\n| ESG评级 | BB-(MSCI) | BBB(S&P) | BB+(MSCI) | BBB-(S&P) |\\n| Q3机构调研次数 | 87次 | 126次 | 53次 | 89次 |\\n| 散户持股比例 | 41% | 38% | 45% | 42% |\\n| 海外项目纠纷数 | 4起 | 2起 | 1起 | 2.3起 |\\n| 研发投入占比 | 0.8% | 1.2% | 0.9% | 1.1% |\\n\\n*数据周期:2025年第三季度,来源:公司公告、各评级机构、沪深交易所、彭博终端*\"",
  121 + "[10:56:41] }"
  122 +]
  123 +
  124 +# MediaEngine反思总结 - 单行JSON格式
  125 +REAL_MEDIA_ENGINE_REFLECTION = """[10:56:15] 2025-11-06 10:56:15.779 | INFO | MediaEngine.nodes.summary_node:run:268 - 正在生成反思总结
  126 +[10:56:42] 2025-11-06 10:56:42.337 | INFO | MediaEngine.nodes.summary_node:process_output:302 - 清理后的输出: {"updated_paragraph_latest_state": "## 综合信息概览\\r\\n根据当前查询需求,本段将围绕洛阳钼业的基本情况展开分析,重点涵盖其公司成立时间、总部位置、主营业务以及在全球矿业领域的地位。尽管本次提供的搜索结果为空,但基于对公开权威信息的掌握和行业常识,结合企业官网、年报及主流财经媒体的历史报道,可以系统性地还原洛阳钼业的核心概况。作为全球领先的多元化矿业集团,洛阳钼业在中国乃至世界有色金属行业中占据重要地位,其发展历程、战略布局与资源控制能力均体现出显著的国际化特征。\\r\\n\\r\\n## 文本内容深度分析\\r\\n洛阳钼业全称为洛阳栾川钼业集团股份有限公司,成立于2003年,其前身可追溯至1969年建立的栾川钼矿,标志着企业在钼钨资源开发领域拥有深厚的历史积淀。公司于2007年在香港联交所主板上市(股票代码:03993.HK),并于2012年在上海证券交易所主板上市(股票代码:603993),形成A+H股双资本平台格局,增强了融资能力和国际影响力。总部位于河南省洛阳市栾川县,地处中国中部重要的矿产资源富集区,依托当地丰富的钼、钨等战略金属储量,构建了从采矿、选矿到深加工的一体化产业链。公司的主营业务聚焦于基本金属和稀有金属的勘探、开采、加工与销售,核心产品包括钼、钨、铜、钴、铌、磷以及黄金等,形成了多元化的矿产品组合,有效提升了抗周期波动的能力。尤其在钼资源方面,洛阳钼业拥有的栾川矿区被誉为'世界三大钼矿之一',其钼金属储量位居全球前列;而在钨资源方面也具备世界级规模,是中国乃至全球最重要的钨生产商之一。近年来,通过一系列跨国并购,公司成功拓展至非洲和南美市场,特别是在刚果(金)运营的Tenke Fungurume铜钴矿,使其成为全球第二大钴生产商,在新能源电池原材料供应链中占据关键地位。此外,公司在巴西持有的铌矿(Catalão和Boa Vista项目)同样是全球高品位铌资源的重要供应源,铌广泛应用于高强度合金钢制造,服务于航空航天与高端装备制造领域。\\r\\n\\r\\n## 视觉信息解读\\r\\n虽然本次未提供相关图片资料,但从以往公开发布的公司宣传材料、年报封面及矿山实景图中可以推断出,洛阳钼业的品牌视觉通常以深蓝、灰色为主色调,象征着工业稳重与科技感,配以矿山开采场景、现代化选矿厂或地球仪元素,突出其'全球化矿业巨头'的定位。例如,在年度报告中常见大型露天矿坑航拍图,展现宏大的开采规模;也有员工在智能化控制中心监控生产流程的画面,体现数字化转型成果。这些视觉符号共同塑造了一个传统资源型企业向高科技、绿色化、国际化综合矿业集团转型的形象。若能获取近期官方发布的图片,预计将看到更多关于绿色矿山建设、生态修复工程以及海外项目本地社区合作的内容,反映ESG(环境、社会与治理)理念的深入实践。\\r\\n\\r\\n## 数据综合分析\\r\\n从财务与运营数据来看,洛阳钼业近年来保持稳健增长态势。根据2023年年报显示,公司全年实现营业收入约1,445亿元人民币,归母净利润超过80亿元,资产总额逾2,000亿元,展现出强大的盈利能力和资产实力。在资源储量方面,据JORC标准披露,公司控制的钼金属储量超过200万吨,钨储量约80万吨,铜资源量达数千万吨级别,钴资源量亦达数百万吨,资源禀赋极为优越。产量方面,2023年公司年产钼约1.7万吨、钨精矿折合WO₃约2.5万吨、铜金属约22万吨、钴金属约2.5万吨,其中铜钴产量主要来自刚果(金)和澳大利亚Northparkes项目。在全球矿业排名中,洛阳钼业连续多年入选《福布斯》全球企业2000强,并在《财富》中国500强中位列前茅。据SNL Metals & Mining等机构统计,其钴产量市场份额约占全球总产量的15%-18%,仅次于嘉能可(Glencore),居世界第二位;而钼产品的市场占有率同样位居全球前三。此外,公司研发投入持续增加,2023年研发费用超15亿元,主要用于智能矿山建设、低品位矿石综合利用技术及碳减排工艺优化,体现了向高质量发展模式转型的决心。\\r\\n\\r\\n## 多维度洞察\\r\\n综上所述,洛阳钼业不仅是一家根植于中国河南的地方性矿业企业,更已发展为具有全球资源配置能力的跨国矿业集团。其成功路径体现出'立足本土优势资源+战略性海外扩张'的双轮驱动模式。在国内,依托栾川世界级钼钨矿床建立了稳固的基本盘;在海外,通过精准并购实现了对关键战略矿产——尤其是新能源所需铜钴资源——的有效掌控,契合全球能源转型趋势。与此同时,公司积极推进数字化、智能化和绿色矿山建设,如在北秘鲁的Kisanfu铜钴矿采用无人驾驶运输系统和远程监控平台,提升安全与效率。未来,随着电动汽车、储能系统和可再生能源基础设施对铜、钴、铌等金属需求的持续攀升,洛阳钼业的战略价值将进一步凸显。然而,其海外运营也面临地缘政治风险、环保合规压力及社区关系管理等挑战,尤其是在刚果(金)等资源丰富但治理相对薄弱的国家。因此,如何平衡经济效益与社会责任、强化可持续发展能力,将是决定其长期竞争力的关键所在。"}"""
  127 +
  128 +# ===== SearchNode输出示例(应该被过滤,不应进入论坛)=====
  129 +
  130 +# SearchNode首次搜索查询 - 多行JSON格式
  131 +SEARCH_NODE_FIRST_SEARCH = [
  132 + "[11:16:35] 2025-11-06 11:16:35.567 | INFO | InsightEngine.nodes.search_node:process_output:97 - 清理后的输出: {",
  133 + "[11:16:35] \"search_query\": \"大家怎么看\"",
  134 + "[11:16:35] \"search_tool\": \"search_topic_globally\"",
  135 + "[11:16:35] \"reasoning\": \"这是搜索查询的推理\"",
  136 + "[11:16:35] \"enable_sentiment\": true",
  137 + "[11:16:35] }"
  138 +]
  139 +
  140 +# SearchNode反思搜索查询 - 单行JSON格式
  141 +SEARCH_NODE_REFLECTION_SEARCH = """[11:17:05] 2025-11-06 11:17:05.547 | INFO | InsightEngine.nodes.search_node:process_output:232 - 清理后的输出: {"search_query": "AI教育 数据泄露 不公平", "search_tool": "search_hot_content", "reasoning": "需要了解近期关于AI教育的热点争议,特别是公众最关心的数据安全和公平性问题,以补充具体案例和真实舆情数据", "time_period": "week", "enable_sentiment": true}"""
  142 +
  143 +# ===== 错误日志示例(应该被过滤,不应进入论坛)=====
  144 +
  145 +# SummaryNode的JSON解析失败错误日志
  146 +SUMMARY_NODE_JSON_ERROR = "[11:55:31] 2025-11-06 11:55:31.763 | ERROR | MediaEngine.nodes.summary_node:process_output:141 - JSON解析失败: Unterminated string starting at: line 1 column 28 (char 27)"
  147 +
  148 +# SummaryNode的JSON修复失败错误日志
  149 +SUMMARY_NODE_JSON_FIX_ERROR = "[11:55:31] 2025-11-06 11:55:31.799 | ERROR | MediaEngine.nodes.summary_node:process_output:149 - JSON修复失败,直接使用清理后的文本"
  150 +
  151 +# SummaryNode的ERROR级别日志(包含nodes.summary_node但不应被捕获)
  152 +SUMMARY_NODE_ERROR_LOG = "[11:55:31] 2025-11-06 11:55:31.763 | ERROR | MediaEngine.nodes.summary_node:process_output:141 - 发生错误:无法处理输出"
  153 +
  154 +# SummaryNode的Traceback错误日志(虽然包含nodes.summary_node,但不应被捕获)
  155 +SUMMARY_NODE_TRACEBACK = """[11:55:31] File "D:\\Programing\\BettaFish\\SingleEngineApp\\..\\MediaEngine\\nodes\\summary_node.py", line 138, in process_output
  156 +[11:55:31] result = json.loads(cleaned_output)"""
  157 +
@@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
4 测试各种日志格式下的解析能力,包括: 4 测试各种日志格式下的解析能力,包括:
5 1. 旧格式:[HH:MM:SS] 5 1. 旧格式:[HH:MM:SS]
6 2. 新格式:loguru默认格式 (YYYY-MM-DD HH:mm:ss.SSS | LEVEL | ...) 6 2. 新格式:loguru默认格式 (YYYY-MM-DD HH:mm:ss.SSS | LEVEL | ...)
  7 +3. 只应当接收FirstSummaryNode、ReflectionSummaryNode等SummaryNode的输出,不应当接收SearchNode的输出
7 """ 8 """
8 9
9 import sys 10 import sys
@@ -83,12 +84,11 @@ class TestLogMonitor: @@ -83,12 +84,11 @@ class TestLogMonitor:
83 assert "JSON内容" in result 84 assert "JSON内容" in result
84 85
85 def test_extract_json_content_new_format_multiline(self): 86 def test_extract_json_content_new_format_multiline(self):
86 - """测试新格式多行JSON提取(关键测试:需要支持loguru格式的时间戳移除)""" 87 + """测试新格式多行JSON提取(支持loguru格式的时间戳移除)"""
87 result = self.monitor.extract_json_content(test_data.NEW_FORMAT_MULTILINE_JSON) 88 result = self.monitor.extract_json_content(test_data.NEW_FORMAT_MULTILINE_JSON)
88 - # 注意:当前代码中的时间戳移除正则只支持 [HH:MM:SS] 格式  
89 - # 这个测试可能会失败,直到修复了时间戳移除逻辑  
90 - # 如果失败,说明需要修改 extract_json_content 中的时间戳移除逻辑  
91 - assert result is not None or True # 暂时允许失败,用于发现问题 89 + assert result is not None
  90 + assert "多行" in result
  91 + assert "JSON内容" in result
92 92
93 def test_extract_json_content_updated_priority(self): 93 def test_extract_json_content_updated_priority(self):
94 """测试updated_paragraph_latest_state优先提取""" 94 """测试updated_paragraph_latest_state优先提取"""
@@ -133,13 +133,11 @@ class TestLogMonitor: @@ -133,13 +133,11 @@ class TestLogMonitor:
133 assert "测试内容" in result 133 assert "测试内容" in result
134 134
135 def test_extract_node_content_new_format(self): 135 def test_extract_node_content_new_format(self):
136 - """测试新格式的节点内容提取(关键测试)""" 136 + """测试新格式的节点内容提取"""
137 line = "2025-11-05 17:42:31.287 | INFO | InsightEngine.nodes.summary_node:process_output:131 - FirstSummaryNode 清理后的输出: 这是测试内容" 137 line = "2025-11-05 17:42:31.287 | INFO | InsightEngine.nodes.summary_node:process_output:131 - FirstSummaryNode 清理后的输出: 这是测试内容"
138 result = self.monitor.extract_node_content(line) 138 result = self.monitor.extract_node_content(line)
139 - # 注意:当前代码中的正则只支持 [HH:MM:SS] 格式  
140 - # 这个测试可能会失败,直到修复了时间戳匹配逻辑  
141 - # 如果失败,说明需要修改 extract_node_content 中的时间戳匹配逻辑  
142 - assert result is not None or True # 暂时允许失败,用于发现问题 139 + assert result is not None
  140 + assert "测试内容" in result
143 141
144 def test_process_lines_for_json_old_format(self): 142 def test_process_lines_for_json_old_format(self):
145 """测试旧格式的完整处理流程""" 143 """测试旧格式的完整处理流程"""
@@ -154,7 +152,7 @@ class TestLogMonitor: @@ -154,7 +152,7 @@ class TestLogMonitor:
154 assert any("多行" in content for content in result) 152 assert any("多行" in content for content in result)
155 153
156 def test_process_lines_for_json_new_format(self): 154 def test_process_lines_for_json_new_format(self):
157 - """测试新格式的完整处理流程(关键测试)""" 155 + """测试新格式的完整处理流程"""
158 lines = [ 156 lines = [
159 test_data.NEW_FORMAT_NON_TARGET, # 应该被忽略 157 test_data.NEW_FORMAT_NON_TARGET, # 应该被忽略
160 test_data.NEW_FORMAT_MULTILINE_JSON[0], 158 test_data.NEW_FORMAT_MULTILINE_JSON[0],
@@ -162,15 +160,15 @@ class TestLogMonitor: @@ -162,15 +160,15 @@ class TestLogMonitor:
162 test_data.NEW_FORMAT_MULTILINE_JSON[2], 160 test_data.NEW_FORMAT_MULTILINE_JSON[2],
163 ] 161 ]
164 result = self.monitor.process_lines_for_json(lines, "insight") 162 result = self.monitor.process_lines_for_json(lines, "insight")
165 - # 注意:这个测试可能会失败,因为当前代码可能无法正确处理新格式  
166 - # 如果失败,说明需要修改 process_lines_for_json 和相关函数  
167 - assert len(result) > 0 or True # 暂时允许失败,用于发现问题 163 + assert len(result) > 0
  164 + assert any("多行" in content for content in result)
  165 + assert any("JSON内容" in content for content in result)
168 166
169 def test_process_lines_for_json_mixed_format(self): 167 def test_process_lines_for_json_mixed_format(self):
170 """测试混合格式的处理""" 168 """测试混合格式的处理"""
171 result = self.monitor.process_lines_for_json(test_data.MIXED_FORMAT_LINES, "insight") 169 result = self.monitor.process_lines_for_json(test_data.MIXED_FORMAT_LINES, "insight")
172 - # 混合格式应该也能处理  
173 - assert len(result) > 0 or True # 暂时允许失败,用于发现问题 170 + assert len(result) > 0
  171 + assert any("混合格式内容" in content for content in result)
174 172
175 def test_is_valuable_content(self): 173 def test_is_valuable_content(self):
176 """测试有价值内容的判断""" 174 """测试有价值内容的判断"""
@@ -183,6 +181,150 @@ class TestLogMonitor: @@ -183,6 +181,150 @@ class TestLogMonitor:
183 181
184 # 空行应该被过滤 182 # 空行应该被过滤
185 assert self.monitor.is_valuable_content("") == False 183 assert self.monitor.is_valuable_content("") == False
  184 +
  185 + def test_extract_json_content_real_query_engine(self):
  186 + """测试QueryEngine实际生产环境日志提取"""
  187 + result = self.monitor.extract_json_content(test_data.REAL_QUERY_ENGINE_REFLECTION)
  188 + assert result is not None
  189 + assert "洛阳栾川钼业集团" in result
  190 + assert "CMOC" in result
  191 + assert "updated_paragraph_latest_state" not in result # 应该已经提取内容,不包含字段名
  192 +
  193 + def test_extract_json_content_real_insight_engine(self):
  194 + """测试InsightEngine实际生产环境日志提取(包含标识行)"""
  195 + # 先测试能否识别标识行
  196 + assert self.monitor.is_target_log_line(test_data.REAL_INSIGHT_ENGINE_REFLECTION[0]) == True # 包含"正在生成反思总结"
  197 + assert self.monitor.is_target_log_line(test_data.REAL_INSIGHT_ENGINE_REFLECTION[1]) == True # 包含nodes.summary_node
  198 +
  199 + # 测试JSON提取(从第二行开始,因为第一行是标识行)
  200 + json_lines = test_data.REAL_INSIGHT_ENGINE_REFLECTION[1:] # 跳过标识行
  201 + result = self.monitor.extract_json_content(json_lines)
  202 + assert result is not None
  203 + assert "核心发现" in result
  204 + assert "更新版" in result
  205 + assert "洛阳钼业2025年第三季度" in result
  206 +
  207 + def test_extract_json_content_real_media_engine(self):
  208 + """测试MediaEngine实际生产环境日志提取(单行JSON)"""
  209 + # MediaEngine是单行JSON格式,需要先分割成行
  210 + lines = test_data.REAL_MEDIA_ENGINE_REFLECTION.split('\n')
  211 +
  212 + # 测试能否识别标识行
  213 + assert self.monitor.is_target_log_line(lines[0]) == True # 包含"正在生成反思总结"
  214 + assert self.monitor.is_target_log_line(lines[1]) == True # 包含nodes.summary_node和"清理后的输出"
  215 +
  216 + # 测试JSON提取(从包含JSON的行开始)
  217 + json_line = lines[1] # 第二行包含完整的单行JSON
  218 + result = self.monitor.extract_json_content([json_line])
  219 + assert result is not None
  220 + assert "综合信息概览" in result
  221 + assert "洛阳钼业" in result
  222 + assert "updated_paragraph_latest_state" not in result # 应该已经提取内容
  223 +
  224 + def test_process_lines_for_json_real_query_engine(self):
  225 + """测试QueryEngine实际日志的完整处理流程"""
  226 + result = self.monitor.process_lines_for_json(test_data.REAL_QUERY_ENGINE_REFLECTION, "query")
  227 + assert len(result) > 0
  228 + assert any("洛阳栾川钼业集团" in content for content in result)
  229 +
  230 + def test_process_lines_for_json_real_insight_engine(self):
  231 + """测试InsightEngine实际日志的完整处理流程(包含标识行)"""
  232 + result = self.monitor.process_lines_for_json(test_data.REAL_INSIGHT_ENGINE_REFLECTION, "insight")
  233 + assert len(result) > 0
  234 + assert any("核心发现" in content for content in result)
  235 + assert any("更新版" in content for content in result)
  236 +
  237 + def test_process_lines_for_json_real_media_engine(self):
  238 + """测试MediaEngine实际日志的完整处理流程(单行JSON)"""
  239 + # 将单行字符串分割成多行
  240 + lines = test_data.REAL_MEDIA_ENGINE_REFLECTION.split('\n')
  241 + result = self.monitor.process_lines_for_json(lines, "media")
  242 + assert len(result) > 0
  243 + assert any("综合信息概览" in content for content in result)
  244 + assert any("洛阳钼业" in content for content in result)
  245 +
  246 + def test_filter_search_node_output(self):
  247 + """测试过滤SearchNode的输出(重要:SearchNode不应进入论坛)"""
  248 + # SearchNode的输出包含"清理后的输出: {",但不包含目标节点模式
  249 + search_lines = test_data.SEARCH_NODE_FIRST_SEARCH
  250 + result = self.monitor.process_lines_for_json(search_lines, "insight")
  251 + # SearchNode的输出应该被过滤,不应该被捕获
  252 + assert len(result) == 0
  253 +
  254 + def test_filter_search_node_output_single_line(self):
  255 + """测试过滤SearchNode的单行JSON输出"""
  256 + # SearchNode的单行JSON格式
  257 + search_line = test_data.SEARCH_NODE_REFLECTION_SEARCH
  258 + result = self.monitor.process_lines_for_json([search_line], "insight")
  259 + # SearchNode的输出应该被过滤
  260 + assert len(result) == 0
  261 +
  262 + def test_search_node_vs_summary_node_mixed(self):
  263 + """测试混合场景:SearchNode和SummaryNode同时存在,只捕获SummaryNode"""
  264 + lines = [
  265 + # SearchNode输出(应该被过滤)
  266 + "[11:16:35] 2025-11-06 11:16:35.567 | INFO | InsightEngine.nodes.search_node:process_output:97 - 清理后的输出: {",
  267 + "[11:16:35] \"search_query\": \"测试查询\"",
  268 + "[11:16:35] }",
  269 + # SummaryNode输出(应该被捕获)
  270 + "[11:17:05] 2025-11-06 11:17:05.547 | INFO | InsightEngine.nodes.summary_node:process_output:131 - 清理后的输出: {",
  271 + "[11:17:05] \"paragraph_latest_state\": \"这是总结内容\"",
  272 + "[11:17:05] }",
  273 + ]
  274 + result = self.monitor.process_lines_for_json(lines, "insight")
  275 + # 应该只捕获SummaryNode的输出,不包含SearchNode的输出
  276 + assert len(result) > 0
  277 + assert any("总结内容" in content for content in result)
  278 + # 确保不包含搜索查询内容
  279 + assert not any("search_query" in content for content in result)
  280 + assert not any("测试查询" in content for content in result)
  281 +
  282 + def test_filter_error_logs_from_summary_node(self):
  283 + """测试过滤SummaryNode的错误日志(重要:错误日志不应进入论坛)"""
  284 + # JSON解析失败错误日志
  285 + assert self.monitor.is_target_log_line(test_data.SUMMARY_NODE_JSON_ERROR) == False
  286 +
  287 + # JSON修复失败错误日志
  288 + assert self.monitor.is_target_log_line(test_data.SUMMARY_NODE_JSON_FIX_ERROR) == False
  289 +
  290 + # ERROR级别日志
  291 + assert self.monitor.is_target_log_line(test_data.SUMMARY_NODE_ERROR_LOG) == False
  292 +
  293 + # Traceback错误日志
  294 + for line in test_data.SUMMARY_NODE_TRACEBACK.split('\n'):
  295 + assert self.monitor.is_target_log_line(line) == False
  296 +
  297 + def test_error_logs_not_captured(self):
  298 + """测试错误日志不会被捕获到论坛"""
  299 + error_lines = [
  300 + test_data.SUMMARY_NODE_JSON_ERROR,
  301 + test_data.SUMMARY_NODE_JSON_FIX_ERROR,
  302 + test_data.SUMMARY_NODE_ERROR_LOG,
  303 + ]
  304 +
  305 + for line in error_lines:
  306 + result = self.monitor.process_lines_for_json([line], "media")
  307 + # 错误日志不应该被捕获
  308 + assert len(result) == 0
  309 +
  310 + def test_mixed_valid_and_error_logs(self):
  311 + """测试混合场景:有效日志和错误日志同时存在,只捕获有效日志"""
  312 + lines = [
  313 + # 错误日志(应该被过滤)
  314 + test_data.SUMMARY_NODE_JSON_ERROR,
  315 + test_data.SUMMARY_NODE_JSON_FIX_ERROR,
  316 + # 有效SummaryNode输出(应该被捕获)
  317 + "[11:55:31] 2025-11-06 11:55:31.762 | INFO | MediaEngine.nodes.summary_node:process_output:134 - 清理后的输出: {",
  318 + "[11:55:31] \"paragraph_latest_state\": \"这是有效的总结内容\"",
  319 + "[11:55:31] }",
  320 + ]
  321 + result = self.monitor.process_lines_for_json(lines, "media")
  322 + # 应该只捕获有效日志,不包含错误日志
  323 + assert len(result) > 0
  324 + assert any("有效的总结内容" in content for content in result)
  325 + # 确保不包含错误信息
  326 + assert not any("JSON解析失败" in content for content in result)
  327 + assert not any("JSON修复失败" in content for content in result)
186 328
187 329
188 def run_tests(): 330 def run_tests():