Toggle navigation
Toggle navigation
This project
Loading...
Sign in
万朱浩
/
Venue-Ops
Go to a project
Toggle navigation
Projects
Groups
Snippets
Help
Toggle navigation pinning
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Authored by
Doiiars
2025-11-06 13:57:56 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
dce63714102c60338106a6f1cd5b93c68c70f57e
dce63714
1 parent
adeedff9
1. 修复论坛通信问题
2. 修复总结报告错误 3. 修复环境变量重新载入问题 4. 添加测试用例 5. 修复论坛主持人问题
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
306 additions
and
57 deletions
ForumEngine/llm_host.py
ForumEngine/monitor.py
InsightEngine/tools/keyword_optimizer.py
ReportEngine/agent.py
ReportEngine/flask_interface.py
app.py
config.py
tests/forum_log_test_data.py
tests/test_monitor.py
ForumEngine/llm_host.py
View file @
dce6371
...
...
@@ -12,7 +12,7 @@ import re
# 添加项目根目录到Python路径以导入config
sys
.
path
.
append
(
os
.
path
.
dirname
(
os
.
path
.
dirname
(
os
.
path
.
abspath
(
__file__
))))
from
config
import
FORUM_HOST_API_KEY
,
FORUM_HOST_BASE_URL
,
FORUM_HOST_MODEL_NAME
from
config
import
settings
# 添加utils目录到Python路径
current_dir
=
os
.
path
.
dirname
(
os
.
path
.
abspath
(
__file__
))
...
...
@@ -35,21 +35,21 @@ class ForumHost:
初始化论坛主持人
Args:
api_key: 硅基流动API密钥,如果不提供则从配置文件读取
base_url: 接口基础地址,默认使用配置文件提供的SiliconFlow地址
api_key: 论坛主持人 LLM API 密钥,如果不提供则从配置文件读取
base_url: 论坛主持人 LLM API 接口基础地址,默认使用配置文件提供的SiliconFlow地址
"""
self
.
api_key
=
api_key
or
FORUM_HOST_API_KEY
self
.
api_key
=
api_key
or
settings
.
FORUM_HOST_API_KEY
if
not
self
.
api_key
:
raise
ValueError
(
"未找到
硅基流动API密钥,请在config.py
中设置FORUM_HOST_API_KEY"
)
raise
ValueError
(
"未找到
论坛主持人API密钥,请在环境变量文件
中设置FORUM_HOST_API_KEY"
)
self
.
base_url
=
base_url
or
FORUM_HOST_BASE_URL
self
.
base_url
=
base_url
or
settings
.
FORUM_HOST_BASE_URL
self
.
client
=
OpenAI
(
api_key
=
self
.
api_key
,
base_url
=
self
.
base_url
)
self
.
model
=
model_name
or
FORUM_HOST_MODEL_NAME
# Use configured model
self
.
model
=
model_name
or
settings
.
FORUM_HOST_MODEL_NAME
# Use configured model
# Track previous summaries to avoid duplicates
self
.
previous_summaries
=
[]
...
...
ForumEngine/monitor.py
View file @
dce6371
...
...
@@ -18,7 +18,7 @@ try:
from
.llm_host
import
generate_host_speech
HOST_AVAILABLE
=
True
except
ImportError
:
logger
.
warning
(
"ForumEngine: 论坛主持人模块未找到,将以纯监控模式运行"
)
logger
.
exception
(
"ForumEngine: 论坛主持人模块未找到,将以纯监控模式运行"
)
HOST_AVAILABLE
=
False
class
LogMonitor
:
...
...
@@ -50,10 +50,20 @@ class LogMonitor:
self
.
host_speech_threshold
=
5
# 每5条agent发言触发一次主持人发言
self
.
is_host_generating
=
False
# 主持人是否正在生成发言
# 目标节点名称 - 直接匹配字符串
self
.
target_nodes
=
[
'FirstSummaryNode'
,
'ReflectionSummaryNode'
# 目标节点识别模式
# 1. 类名(旧格式可能包含)
# 2. 完整模块路径(实际日志格式,包含引擎前缀)
# 3. 部分模块路径(兼容性)
# 4. 关键标识文本
self
.
target_node_patterns
=
[
'FirstSummaryNode'
,
# 类名
'ReflectionSummaryNode'
,
# 类名
'InsightEngine.nodes.summary_node'
,
# InsightEngine完整路径
'MediaEngine.nodes.summary_node'
,
# MediaEngine完整路径
'QueryEngine.nodes.summary_node'
,
# QueryEngine完整路径
'nodes.summary_node'
,
# 模块路径(兼容性,用于部分匹配)
'正在生成首次段落总结'
,
# FirstSummaryNode的标识
'正在生成反思总结'
,
# ReflectionSummaryNode的标识
]
# 多行内容捕获状态
...
...
@@ -109,10 +119,31 @@ class LogMonitor:
logger
.
exception
(
f
"ForumEngine: 写入forum.log失败: {e}"
)
def
is_target_log_line
(
self
,
line
:
str
)
->
bool
:
"""检查是否是目标日志行(SummaryNode)"""
# 简单字符串包含检查,更可靠
for
node_name
in
self
.
target_nodes
:
if
node_name
in
line
:
"""检查是否是目标日志行(SummaryNode)
支持多种识别方式:
1. 类名:FirstSummaryNode, ReflectionSummaryNode
2. 完整模块路径:InsightEngine.nodes.summary_node、MediaEngine.nodes.summary_node、QueryEngine.nodes.summary_node
3. 部分模块路径:nodes.summary_node(兼容性)
4. 关键标识文本:正在生成首次段落总结、正在生成反思总结
排除条件:
- ERROR 级别的日志(错误日志不应被识别为目标节点)
- 包含错误关键词的日志(JSON解析失败、JSON修复失败等)
"""
# 排除 ERROR 级别的日志
if
"| ERROR"
in
line
or
"| ERROR |"
in
line
:
return
False
# 排除包含错误关键词的日志
error_keywords
=
[
"JSON解析失败"
,
"JSON修复失败"
,
"Traceback"
,
"File
\"
"
]
for
keyword
in
error_keywords
:
if
keyword
in
line
:
return
False
# 检查是否包含目标节点模式
for
pattern
in
self
.
target_node_patterns
:
if
pattern
in
line
:
return
True
return
False
...
...
@@ -381,13 +412,14 @@ class LogMonitor:
if
not
line
.
strip
():
continue
# 检查是否是目标节点行
或包含JSON开始标记的行
# 检查是否是目标节点行
和JSON开始标记
is_target
=
self
.
is_target_log_line
(
line
)
is_json_start
=
self
.
is_json_start_line
(
line
)
if
is_target
or
is_json_start
:
if
is_json_start
:
# 开始捕获JSON(即使不是目标节点,只要包含"清理后的输出: {"就处理)
# 只有目标节点(SummaryNode)的JSON输出才应该被捕获
# 过滤掉SearchNode等其他节点的输出(它们不是目标节点,即使有JSON也不会被捕获)
if
is_target
and
is_json_start
:
# 开始捕获JSON(必须是目标节点且包含"清理后的输出: {")
self
.
capturing_json
[
app_name
]
=
True
self
.
json_buffer
[
app_name
]
=
[
line
]
self
.
json_start_line
[
app_name
]
=
line
...
...
@@ -528,7 +560,10 @@ class LogMonitor:
# 先检查是否需要触发搜索(只触发一次)
if
not
self
.
is_searching
:
for
line
in
new_lines
:
if
line
.
strip
()
and
'FirstSummaryNode'
in
line
:
# 检查是否包含目标节点模式(支持多种格式)
if
line
.
strip
()
and
self
.
is_target_log_line
(
line
):
# 进一步确认是首次总结节点(FirstSummaryNode或包含"正在生成首次段落总结")
if
'FirstSummaryNode'
in
line
or
'正在生成首次段落总结'
in
line
:
logger
.
info
(
f
"ForumEngine: 在{app_name}中检测到第一次论坛发表内容"
)
self
.
is_searching
=
True
self
.
search_inactive_count
=
0
...
...
InsightEngine/tools/keyword_optimizer.py
View file @
dce6371
...
...
@@ -161,6 +161,11 @@ class KeywordOptimizer:
**重要提醒**:每个关键词都必须是一个不可分割的独立词条,严禁在词条内部包含空格。例如,应使用 "雷军班争议" 而不是错误的 "雷军班 争议"。
**主题相关**:关键词要与初始查询主题相关,不要偏离主题
- 保留主体词("武汉天气"→"武汉"✓)
- 避免泛化属性("天气"✗ 会匹配全国)
- 专有名词可拆("武汉大学"→"武大"、"大学"✓)
**输出格式**:
请以JSON格式返回结果:
{
...
...
ReportEngine/agent.py
View file @
dce6371
...
...
@@ -195,7 +195,7 @@ class ReportAgent:
start_time
=
datetime
.
now
()
logger
.
info
(
f
"开始生成报告: {query}"
)
self
.
logger
.
info
(
f
"输入数据 - 报告数量: {len(reports)}, 论坛日志长度: {len(forum_logs)}"
)
logger
.
info
(
f
"输入数据 - 报告数量: {len(reports)}, 论坛日志长度: {len(forum_logs)}"
)
try
:
# Step 1: 模板选择
...
...
ReportEngine/flask_interface.py
View file @
dce6371
...
...
@@ -134,6 +134,7 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = "
task
.
update_status
(
"completed"
,
100
)
except
Exception
as
e
:
logger
.
exception
(
f
"报告生成过程中发生错误: {str(e)}"
)
task
.
update_status
(
"error"
,
0
,
str
(
e
))
# 只在出错时清理任务
with
task_lock
:
...
...
@@ -156,6 +157,7 @@ def get_status():
'current_task'
:
current_task
.
to_dict
()
if
current_task
else
None
})
except
Exception
as
e
:
logger
.
exception
(
f
"获取Report Engine状态失败: {str(e)}"
)
return
jsonify
({
'success'
:
False
,
'error'
:
str
(
e
)
...
...
@@ -228,6 +230,7 @@ def generate_report():
})
except
Exception
as
e
:
logger
.
exception
(
f
"开始生成报告失败: {str(e)}"
)
return
jsonify
({
'success'
:
False
,
'error'
:
str
(
e
)
...
...
@@ -319,6 +322,7 @@ def get_result_json(task_id: str):
})
except
Exception
as
e
:
logger
.
exception
(
f
"获取报告生成结果失败: {str(e)}"
)
return
jsonify
({
'success'
:
False
,
'error'
:
str
(
e
)
...
...
@@ -348,6 +352,7 @@ def cancel_task(task_id: str):
}),
404
except
Exception
as
e
:
logger
.
exception
(
f
"取消报告生成任务失败: {str(e)}"
)
return
jsonify
({
'success'
:
False
,
'error'
:
str
(
e
)
...
...
@@ -391,6 +396,7 @@ def get_templates():
})
except
Exception
as
e
:
logger
.
exception
(
f
"获取可用模板列表失败: {str(e)}"
)
return
jsonify
({
'success'
:
False
,
'error'
:
str
(
e
)
...
...
@@ -400,6 +406,7 @@ def get_templates():
# 错误处理
@report_bp.errorhandler
(
404
)
def
not_found
(
error
):
logger
.
exception
(
f
"API端点不存在: {str(error)}"
)
return
jsonify
({
'success'
:
False
,
'error'
:
'API端点不存在'
...
...
@@ -408,6 +415,7 @@ def not_found(error):
@report_bp.errorhandler
(
500
)
def
internal_error
(
error
):
logger
.
exception
(
f
"服务器内部错误: {str(error)}"
)
return
jsonify
({
'success'
:
False
,
'error'
:
'服务器内部错误'
...
...
app.py
View file @
dce6371
...
...
@@ -94,20 +94,10 @@ def _load_config_module():
def
read_config_values
():
"""Return the current configuration values that are exposed to the frontend."""
try
:
# 重新导入 config 模块以获取最新的 Settings 实例
importlib
.
invalidate_caches
()
if
CONFIG_MODULE_NAME
in
sys
.
modules
:
importlib
.
reload
(
sys
.
modules
[
CONFIG_MODULE_NAME
])
else
:
importlib
.
import_module
(
CONFIG_MODULE_NAME
)
# 从 config 模块获取 settings 实例
config_module
=
sys
.
modules
[
CONFIG_MODULE_NAME
]
if
not
hasattr
(
config_module
,
'settings'
):
logger
.
error
(
"config 模块中没有找到 settings 实例"
)
return
{}
# 重新加载配置以获取最新的 Settings 实例
from
config
import
reload_settings
,
settings
reload_settings
()
settings
=
config_module
.
settings
values
=
{}
for
key
in
CONFIG_KEYS
:
# 从 Pydantic Settings 实例读取值
...
...
config.py
View file @
dce6371
...
...
@@ -9,8 +9,9 @@
from
pathlib
import
Path
from
pydantic_settings
import
BaseSettings
from
pydantic
import
Field
from
pydantic
import
Field
,
ConfigDict
from
typing
import
Optional
from
loguru
import
logger
# 计算 .env 优先级:优先当前工作目录,其次项目根目录
...
...
@@ -86,12 +87,29 @@ class Settings(BaseSettings):
SEARCH_TIMEOUT
:
int
=
Field
(
240
,
description
=
"单次搜索请求超时"
)
MAX_CONTENT_LENGTH
:
int
=
Field
(
500000
,
description
=
"搜索最大内容长度"
)
class
Config
:
env_file
=
ENV_FILE
env_prefix
=
""
case_sensitive
=
False
extra
=
"allow"
model_config
=
ConfigDict
(
env_file
=
ENV_FILE
,
env_prefix
=
""
,
case_sensitive
=
False
,
extra
=
"allow"
)
# 创建全局配置实例
settings
=
Settings
()
def
reload_settings
()
->
Settings
:
"""
重新加载配置
从 .env 文件和环境变量重新加载配置,更新全局 settings 实例。
用于在运行时动态更新配置。
Returns:
Settings: 新创建的配置实例
"""
global
settings
settings
=
Settings
()
return
settings
...
...
tests/forum_log_test_data.py
View file @
dce6371
...
...
@@ -104,3 +104,54 @@ MIXED_FORMAT_LINES = [
"[17:42:31] }"
]
# ===== 实际生产环境日志示例 =====
# QueryEngine反思总结 - 多行JSON格式
REAL_QUERY_ENGINE_REFLECTION
=
[
"[10:56:04] 2025-11-06 10:56:04.759 | INFO | QueryEngine.nodes.summary_node:process_output:302 - 清理后的输出: {"
,
"[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亿元,为战略布局提供充足弹药。
\"
"
,
"[10:56:04] }"
]
# InsightEngine反思总结 - 多行JSON格式(包含"正在生成反思总结"标识)
REAL_INSIGHT_ENGINE_REFLECTION
=
[
"[10:55:19] 2025-11-06 10:55:19.563 | INFO | InsightEngine.nodes.summary_node:run:265 - 正在生成反思总结"
,
"[10:56:41] 2025-11-06 10:56:41.626 | INFO | InsightEngine.nodes.summary_node:process_output:296 - 清理后的输出: {"
,
"[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年第三季度,来源:公司公告、各评级机构、沪深交易所、彭博终端*
\"
"
,
"[10:56:41] }"
]
# MediaEngine反思总结 - 单行JSON格式
REAL_MEDIA_ENGINE_REFLECTION
=
"""[10:56:15] 2025-11-06 10:56:15.779 | INFO | MediaEngine.nodes.summary_node:run:268 - 正在生成反思总结
[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铜钴矿采用无人驾驶运输系统和远程监控平台,提升安全与效率。未来,随着电动汽车、储能系统和可再生能源基础设施对铜、钴、铌等金属需求的持续攀升,洛阳钼业的战略价值将进一步凸显。然而,其海外运营也面临地缘政治风险、环保合规压力及社区关系管理等挑战,尤其是在刚果(金)等资源丰富但治理相对薄弱的国家。因此,如何平衡经济效益与社会责任、强化可持续发展能力,将是决定其长期竞争力的关键所在。"}"""
# ===== SearchNode输出示例(应该被过滤,不应进入论坛)=====
# SearchNode首次搜索查询 - 多行JSON格式
SEARCH_NODE_FIRST_SEARCH
=
[
"[11:16:35] 2025-11-06 11:16:35.567 | INFO | InsightEngine.nodes.search_node:process_output:97 - 清理后的输出: {"
,
"[11:16:35]
\"
search_query
\"
:
\"
大家怎么看
\"
"
,
"[11:16:35]
\"
search_tool
\"
:
\"
search_topic_globally
\"
"
,
"[11:16:35]
\"
reasoning
\"
:
\"
这是搜索查询的推理
\"
"
,
"[11:16:35]
\"
enable_sentiment
\"
: true"
,
"[11:16:35] }"
]
# SearchNode反思搜索查询 - 单行JSON格式
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}"""
# ===== 错误日志示例(应该被过滤,不应进入论坛)=====
# SummaryNode的JSON解析失败错误日志
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)"
# SummaryNode的JSON修复失败错误日志
SUMMARY_NODE_JSON_FIX_ERROR
=
"[11:55:31] 2025-11-06 11:55:31.799 | ERROR | MediaEngine.nodes.summary_node:process_output:149 - JSON修复失败,直接使用清理后的文本"
# SummaryNode的ERROR级别日志(包含nodes.summary_node但不应被捕获)
SUMMARY_NODE_ERROR_LOG
=
"[11:55:31] 2025-11-06 11:55:31.763 | ERROR | MediaEngine.nodes.summary_node:process_output:141 - 发生错误:无法处理输出"
# SummaryNode的Traceback错误日志(虽然包含nodes.summary_node,但不应被捕获)
SUMMARY_NODE_TRACEBACK
=
"""[11:55:31] File "D:
\\
Programing
\\
BettaFish
\\
SingleEngineApp
\\
..
\\
MediaEngine
\\
nodes
\\
summary_node.py", line 138, in process_output
[11:55:31] result = json.loads(cleaned_output)"""
...
...
tests/test_monitor.py
View file @
dce6371
...
...
@@ -4,6 +4,7 @@
测试各种日志格式下的解析能力,包括:
1. 旧格式:[HH:MM:SS]
2. 新格式:loguru默认格式 (YYYY-MM-DD HH:mm:ss.SSS | LEVEL | ...)
3. 只应当接收FirstSummaryNode、ReflectionSummaryNode等SummaryNode的输出,不应当接收SearchNode的输出
"""
import
sys
...
...
@@ -83,12 +84,11 @@ class TestLogMonitor:
assert
"JSON内容"
in
result
def
test_extract_json_content_new_format_multiline
(
self
):
"""测试新格式多行JSON提取(
关键测试:需要
支持loguru格式的时间戳移除)"""
"""测试新格式多行JSON提取(支持loguru格式的时间戳移除)"""
result
=
self
.
monitor
.
extract_json_content
(
test_data
.
NEW_FORMAT_MULTILINE_JSON
)
# 注意:当前代码中的时间戳移除正则只支持 [HH:MM:SS] 格式
# 这个测试可能会失败,直到修复了时间戳移除逻辑
# 如果失败,说明需要修改 extract_json_content 中的时间戳移除逻辑
assert
result
is
not
None
or
True
# 暂时允许失败,用于发现问题
assert
result
is
not
None
assert
"多行"
in
result
assert
"JSON内容"
in
result
def
test_extract_json_content_updated_priority
(
self
):
"""测试updated_paragraph_latest_state优先提取"""
...
...
@@ -133,13 +133,11 @@ class TestLogMonitor:
assert
"测试内容"
in
result
def
test_extract_node_content_new_format
(
self
):
"""测试新格式的节点内容提取
(关键测试)
"""
"""测试新格式的节点内容提取"""
line
=
"2025-11-05 17:42:31.287 | INFO | InsightEngine.nodes.summary_node:process_output:131 - FirstSummaryNode 清理后的输出: 这是测试内容"
result
=
self
.
monitor
.
extract_node_content
(
line
)
# 注意:当前代码中的正则只支持 [HH:MM:SS] 格式
# 这个测试可能会失败,直到修复了时间戳匹配逻辑
# 如果失败,说明需要修改 extract_node_content 中的时间戳匹配逻辑
assert
result
is
not
None
or
True
# 暂时允许失败,用于发现问题
assert
result
is
not
None
assert
"测试内容"
in
result
def
test_process_lines_for_json_old_format
(
self
):
"""测试旧格式的完整处理流程"""
...
...
@@ -154,7 +152,7 @@ class TestLogMonitor:
assert
any
(
"多行"
in
content
for
content
in
result
)
def
test_process_lines_for_json_new_format
(
self
):
"""测试新格式的完整处理流程
(关键测试)
"""
"""测试新格式的完整处理流程"""
lines
=
[
test_data
.
NEW_FORMAT_NON_TARGET
,
# 应该被忽略
test_data
.
NEW_FORMAT_MULTILINE_JSON
[
0
],
...
...
@@ -162,15 +160,15 @@ class TestLogMonitor:
test_data
.
NEW_FORMAT_MULTILINE_JSON
[
2
],
]
result
=
self
.
monitor
.
process_lines_for_json
(
lines
,
"insight"
)
# 注意:这个测试可能会失败,因为当前代码可能无法正确处理新格式
# 如果失败,说明需要修改 process_lines_for_json 和相关函数
assert
len
(
result
)
>
0
or
True
# 暂时允许失败,用于发现问题
assert
len
(
result
)
>
0
assert
any
(
"多行"
in
content
for
content
in
result
)
assert
any
(
"JSON内容"
in
content
for
content
in
result
)
def
test_process_lines_for_json_mixed_format
(
self
):
"""测试混合格式的处理"""
result
=
self
.
monitor
.
process_lines_for_json
(
test_data
.
MIXED_FORMAT_LINES
,
"insight"
)
# 混合格式应该也能处理
assert
len
(
result
)
>
0
or
True
# 暂时允许失败,用于发现问题
assert
len
(
result
)
>
0
assert
any
(
"混合格式内容"
in
content
for
content
in
result
)
def
test_is_valuable_content
(
self
):
"""测试有价值内容的判断"""
...
...
@@ -184,6 +182,150 @@ class TestLogMonitor:
# 空行应该被过滤
assert
self
.
monitor
.
is_valuable_content
(
""
)
==
False
def
test_extract_json_content_real_query_engine
(
self
):
"""测试QueryEngine实际生产环境日志提取"""
result
=
self
.
monitor
.
extract_json_content
(
test_data
.
REAL_QUERY_ENGINE_REFLECTION
)
assert
result
is
not
None
assert
"洛阳栾川钼业集团"
in
result
assert
"CMOC"
in
result
assert
"updated_paragraph_latest_state"
not
in
result
# 应该已经提取内容,不包含字段名
def
test_extract_json_content_real_insight_engine
(
self
):
"""测试InsightEngine实际生产环境日志提取(包含标识行)"""
# 先测试能否识别标识行
assert
self
.
monitor
.
is_target_log_line
(
test_data
.
REAL_INSIGHT_ENGINE_REFLECTION
[
0
])
==
True
# 包含"正在生成反思总结"
assert
self
.
monitor
.
is_target_log_line
(
test_data
.
REAL_INSIGHT_ENGINE_REFLECTION
[
1
])
==
True
# 包含nodes.summary_node
# 测试JSON提取(从第二行开始,因为第一行是标识行)
json_lines
=
test_data
.
REAL_INSIGHT_ENGINE_REFLECTION
[
1
:]
# 跳过标识行
result
=
self
.
monitor
.
extract_json_content
(
json_lines
)
assert
result
is
not
None
assert
"核心发现"
in
result
assert
"更新版"
in
result
assert
"洛阳钼业2025年第三季度"
in
result
def
test_extract_json_content_real_media_engine
(
self
):
"""测试MediaEngine实际生产环境日志提取(单行JSON)"""
# MediaEngine是单行JSON格式,需要先分割成行
lines
=
test_data
.
REAL_MEDIA_ENGINE_REFLECTION
.
split
(
'
\n
'
)
# 测试能否识别标识行
assert
self
.
monitor
.
is_target_log_line
(
lines
[
0
])
==
True
# 包含"正在生成反思总结"
assert
self
.
monitor
.
is_target_log_line
(
lines
[
1
])
==
True
# 包含nodes.summary_node和"清理后的输出"
# 测试JSON提取(从包含JSON的行开始)
json_line
=
lines
[
1
]
# 第二行包含完整的单行JSON
result
=
self
.
monitor
.
extract_json_content
([
json_line
])
assert
result
is
not
None
assert
"综合信息概览"
in
result
assert
"洛阳钼业"
in
result
assert
"updated_paragraph_latest_state"
not
in
result
# 应该已经提取内容
def
test_process_lines_for_json_real_query_engine
(
self
):
"""测试QueryEngine实际日志的完整处理流程"""
result
=
self
.
monitor
.
process_lines_for_json
(
test_data
.
REAL_QUERY_ENGINE_REFLECTION
,
"query"
)
assert
len
(
result
)
>
0
assert
any
(
"洛阳栾川钼业集团"
in
content
for
content
in
result
)
def
test_process_lines_for_json_real_insight_engine
(
self
):
"""测试InsightEngine实际日志的完整处理流程(包含标识行)"""
result
=
self
.
monitor
.
process_lines_for_json
(
test_data
.
REAL_INSIGHT_ENGINE_REFLECTION
,
"insight"
)
assert
len
(
result
)
>
0
assert
any
(
"核心发现"
in
content
for
content
in
result
)
assert
any
(
"更新版"
in
content
for
content
in
result
)
def
test_process_lines_for_json_real_media_engine
(
self
):
"""测试MediaEngine实际日志的完整处理流程(单行JSON)"""
# 将单行字符串分割成多行
lines
=
test_data
.
REAL_MEDIA_ENGINE_REFLECTION
.
split
(
'
\n
'
)
result
=
self
.
monitor
.
process_lines_for_json
(
lines
,
"media"
)
assert
len
(
result
)
>
0
assert
any
(
"综合信息概览"
in
content
for
content
in
result
)
assert
any
(
"洛阳钼业"
in
content
for
content
in
result
)
def
test_filter_search_node_output
(
self
):
"""测试过滤SearchNode的输出(重要:SearchNode不应进入论坛)"""
# SearchNode的输出包含"清理后的输出: {",但不包含目标节点模式
search_lines
=
test_data
.
SEARCH_NODE_FIRST_SEARCH
result
=
self
.
monitor
.
process_lines_for_json
(
search_lines
,
"insight"
)
# SearchNode的输出应该被过滤,不应该被捕获
assert
len
(
result
)
==
0
def
test_filter_search_node_output_single_line
(
self
):
"""测试过滤SearchNode的单行JSON输出"""
# SearchNode的单行JSON格式
search_line
=
test_data
.
SEARCH_NODE_REFLECTION_SEARCH
result
=
self
.
monitor
.
process_lines_for_json
([
search_line
],
"insight"
)
# SearchNode的输出应该被过滤
assert
len
(
result
)
==
0
def
test_search_node_vs_summary_node_mixed
(
self
):
"""测试混合场景:SearchNode和SummaryNode同时存在,只捕获SummaryNode"""
lines
=
[
# SearchNode输出(应该被过滤)
"[11:16:35] 2025-11-06 11:16:35.567 | INFO | InsightEngine.nodes.search_node:process_output:97 - 清理后的输出: {"
,
"[11:16:35]
\"
search_query
\"
:
\"
测试查询
\"
"
,
"[11:16:35] }"
,
# SummaryNode输出(应该被捕获)
"[11:17:05] 2025-11-06 11:17:05.547 | INFO | InsightEngine.nodes.summary_node:process_output:131 - 清理后的输出: {"
,
"[11:17:05]
\"
paragraph_latest_state
\"
:
\"
这是总结内容
\"
"
,
"[11:17:05] }"
,
]
result
=
self
.
monitor
.
process_lines_for_json
(
lines
,
"insight"
)
# 应该只捕获SummaryNode的输出,不包含SearchNode的输出
assert
len
(
result
)
>
0
assert
any
(
"总结内容"
in
content
for
content
in
result
)
# 确保不包含搜索查询内容
assert
not
any
(
"search_query"
in
content
for
content
in
result
)
assert
not
any
(
"测试查询"
in
content
for
content
in
result
)
def
test_filter_error_logs_from_summary_node
(
self
):
"""测试过滤SummaryNode的错误日志(重要:错误日志不应进入论坛)"""
# JSON解析失败错误日志
assert
self
.
monitor
.
is_target_log_line
(
test_data
.
SUMMARY_NODE_JSON_ERROR
)
==
False
# JSON修复失败错误日志
assert
self
.
monitor
.
is_target_log_line
(
test_data
.
SUMMARY_NODE_JSON_FIX_ERROR
)
==
False
# ERROR级别日志
assert
self
.
monitor
.
is_target_log_line
(
test_data
.
SUMMARY_NODE_ERROR_LOG
)
==
False
# Traceback错误日志
for
line
in
test_data
.
SUMMARY_NODE_TRACEBACK
.
split
(
'
\n
'
):
assert
self
.
monitor
.
is_target_log_line
(
line
)
==
False
def
test_error_logs_not_captured
(
self
):
"""测试错误日志不会被捕获到论坛"""
error_lines
=
[
test_data
.
SUMMARY_NODE_JSON_ERROR
,
test_data
.
SUMMARY_NODE_JSON_FIX_ERROR
,
test_data
.
SUMMARY_NODE_ERROR_LOG
,
]
for
line
in
error_lines
:
result
=
self
.
monitor
.
process_lines_for_json
([
line
],
"media"
)
# 错误日志不应该被捕获
assert
len
(
result
)
==
0
def
test_mixed_valid_and_error_logs
(
self
):
"""测试混合场景:有效日志和错误日志同时存在,只捕获有效日志"""
lines
=
[
# 错误日志(应该被过滤)
test_data
.
SUMMARY_NODE_JSON_ERROR
,
test_data
.
SUMMARY_NODE_JSON_FIX_ERROR
,
# 有效SummaryNode输出(应该被捕获)
"[11:55:31] 2025-11-06 11:55:31.762 | INFO | MediaEngine.nodes.summary_node:process_output:134 - 清理后的输出: {"
,
"[11:55:31]
\"
paragraph_latest_state
\"
:
\"
这是有效的总结内容
\"
"
,
"[11:55:31] }"
,
]
result
=
self
.
monitor
.
process_lines_for_json
(
lines
,
"media"
)
# 应该只捕获有效日志,不包含错误日志
assert
len
(
result
)
>
0
assert
any
(
"有效的总结内容"
in
content
for
content
in
result
)
# 确保不包含错误信息
assert
not
any
(
"JSON解析失败"
in
content
for
content
in
result
)
assert
not
any
(
"JSON修复失败"
in
content
for
content
in
result
)
def
run_tests
():
"""运行所有测试"""
...
...
Please
register
or
login
to post a comment