Skip to content

Commit ac6f115

Browse files
committed
Fix market review catalyst snippets (ZhuLinsen#1241)
* fix: preserve market review catalyst details
1 parent 9acc6eb commit ac6f115

3 files changed

Lines changed: 117 additions & 21 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
2525
- [测试] 补充设置项帮助元数据、API schema、前端弹窗交互测试,并修复 Bot 名称路由与调度时间 provider 测试的离线 CI 稳定性问题。
2626
- [修复] 港股日线跳过不支持港股的内置历史数据源,避免港股代码错配到非港股市场数据。
2727
- [修复] 修正分析 API 对北交所 `BJ` 前缀与 `.BJ` 后缀股票代码的校验,保持前端自动补全与 Tushare `ts_code` 调用格式一致。
28+
- [修复] 大盘复盘“近三日催化线索”改为明确展示摘要片段、来源日期和 URL,避免把搜索摘要截断内容误呈现为完整事件。
2829

2930
## [3.15.0] - 2026-05-05
3031

src/market_analyzer.py

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -728,34 +728,61 @@ def _build_sector_block(self, overview: MarketOverview) -> str:
728728
return "\n".join(lines)
729729

730730
def _build_news_block(self, news: List) -> str:
731-
"""Build a compact news catalyst table for the rendered report."""
731+
"""Build a source-aware news catalyst table for the rendered report."""
732732
if not news:
733733
return ""
734734
if self._get_review_language() == "en":
735735
lines = [
736736
"#### News Catalysts",
737-
"| # | Headline | Signal |",
738-
"|---|----------|--------|",
737+
"| # | Headline | Snippet / Lead | Source |",
738+
"|---|----------|----------------|--------|",
739739
]
740740
else:
741741
lines = [
742742
"#### 近三日催化线索",
743-
"| 序号 | 事件/标题 | 关注点 |",
744-
"|------|-----------|--------|",
743+
"| 序号 | 事件/标题 | 摘要/线索片段 | 来源 |",
744+
"|------|-----------|----------------|------|",
745745
]
746746

747747
for idx, item in enumerate(news[:5], 1):
748-
if hasattr(item, "title"):
749-
title = getattr(item, "title", "") or "-"
750-
snippet = getattr(item, "snippet", "") or ""
751-
else:
752-
title = item.get("title", "-") or "-"
753-
snippet = item.get("snippet", "") or ""
754-
title = self._escape_table_cell(str(title).strip()[:42])
755-
signal = self._escape_table_cell(str(snippet).strip().replace("\n", " ")[:58] or "-")
756-
lines.append(f"| {idx} | {title} | {signal} |")
748+
title = self._escape_table_cell(
749+
self._compact_news_text(self._get_news_field(item, "title"), limit=80) or "-"
750+
)
751+
snippet = self._escape_table_cell(
752+
self._compact_news_text(self._get_news_field(item, "snippet"), limit=180) or "-"
753+
)
754+
source = self._escape_table_cell(self._format_news_source_cell(item) or "-")
755+
lines.append(f"| {idx} | {title} | {snippet} | {source} |")
757756
return "\n".join(lines)
758757

758+
@staticmethod
759+
def _get_news_field(item: Any, field: str) -> str:
760+
if hasattr(item, field):
761+
value = getattr(item, field, "") or ""
762+
elif isinstance(item, dict):
763+
value = item.get(field, "") or ""
764+
else:
765+
value = ""
766+
return str(value).strip()
767+
768+
@classmethod
769+
def _format_news_source_cell(cls, item: Any) -> str:
770+
source = cls._compact_news_text(cls._get_news_field(item, "source"), limit=40)
771+
date_text = cls._compact_news_text(cls._get_news_field(item, "published_date"), limit=24)
772+
url = cls._compact_news_text(cls._get_news_field(item, "url"), limit=0)
773+
label_parts = [part for part in (source, date_text) if part]
774+
label = " / ".join(label_parts)
775+
if url:
776+
return f"[{label or 'URL'}]({url})"
777+
return label
778+
779+
@staticmethod
780+
def _compact_news_text(value: str, *, limit: int) -> str:
781+
text = " ".join(str(value or "").split())
782+
if limit <= 0 or len(text) <= limit:
783+
return text
784+
return text[: max(0, limit - 3)].rstrip() + "..."
785+
759786
@staticmethod
760787
def _format_optional_number(value: float) -> str:
761788
return "N/A" if value in (None, 0, 0.0) else f"{value:.2f}"
@@ -847,13 +874,15 @@ def _build_review_prompt(self, overview: MarketOverview, news: List) -> str:
847874
news_text = ""
848875
for i, n in enumerate(news[:6], 1):
849876
# 兼容 SearchResult 对象和字典
850-
if hasattr(n, 'title'):
851-
title = n.title[:50] if n.title else ''
852-
snippet = n.snippet[:100] if n.snippet else ''
853-
else:
854-
title = n.get('title', '')[:50]
855-
snippet = n.get('snippet', '')[:100]
856-
news_text += f"{i}. {title}\n {snippet}\n"
877+
title = self._compact_news_text(self._get_news_field(n, "title"), limit=90)
878+
snippet = self._compact_news_text(self._get_news_field(n, "snippet"), limit=220)
879+
source = self._compact_news_text(self._get_news_field(n, "source"), limit=60)
880+
published_date = self._compact_news_text(self._get_news_field(n, "published_date"), limit=30)
881+
url = self._compact_news_text(self._get_news_field(n, "url"), limit=180)
882+
meta_parts = [part for part in (source, published_date) if part]
883+
meta = f" ({' / '.join(meta_parts)})" if meta_parts else ""
884+
url_line = f"\n URL: {url}" if url else ""
885+
news_text += f"{i}. {title}{meta}\n {snippet or '-'}{url_line}\n"
857886

858887
# 按 region 组装市场概况与板块区块(美股无涨跌家数、板块数据)
859888
stats_block = ""

tests/test_market_analyzer_generate_text.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,72 @@ def test_inject_data_into_review_matches_reference_style_chinese_headings(self):
840840
assert "#### 近三日催化线索" in result
841841
assert "AI算力板块走强" in result
842842

843+
def test_news_block_labels_snippets_and_preserves_source_url(self):
844+
from src.market_analyzer import MarketAnalyzer
845+
846+
ma = MarketAnalyzer.__new__(MarketAnalyzer)
847+
ma.config = SimpleNamespace(report_language="zh")
848+
ma.region = "cn"
849+
long_snippet = (
850+
"复盘必读 2026-05-06 复盘的意义在于更清晰地把握市场脉搏,"
851+
"综合描述 A 股三大指数今日集体反弹,成交额放大,科技成长方向领涨。"
852+
)
853+
854+
result = ma._build_news_block([
855+
{
856+
"title": "A股收评:科创50指数放量反弹涨5.47% 两市成交额重回3万亿元",
857+
"snippet": long_snippet,
858+
"source": "东方财富",
859+
"published_date": "2026-05-06",
860+
"url": "https://example.com/news/1",
861+
}
862+
])
863+
864+
assert "摘要/线索片段" in result
865+
assert "关注点" not in result
866+
assert "成交额放大" in result
867+
assert "[东方财富 / 2026-05-06](https://example.com/news/1)" in result
868+
869+
def test_news_block_uses_dash_when_source_metadata_missing(self):
870+
from src.market_analyzer import MarketAnalyzer
871+
872+
ma = MarketAnalyzer.__new__(MarketAnalyzer)
873+
ma.config = SimpleNamespace(report_language="zh")
874+
ma.region = "cn"
875+
876+
result = ma._build_news_block([
877+
{
878+
"title": "政策利好带动板块活跃",
879+
"snippet": "相关主题成交放大",
880+
}
881+
])
882+
883+
assert "| 1 | 政策利好带动板块活跃 | 相关主题成交放大 | - |" in result
884+
assert "| 1 | 政策利好带动板块活跃 | 相关主题成交放大 | source |" not in result
885+
886+
def test_review_prompt_caps_news_url_context(self):
887+
from src.market_analyzer import MarketOverview
888+
889+
ma = self._make_market_analyzer_with_mock_generate_text(return_value="review")
890+
long_url = "https://example.com/redirect?" + "utm_campaign=" + ("x" * 420)
891+
892+
prompt = ma._build_review_prompt(
893+
MarketOverview(date="2026-05-06"),
894+
[
895+
{
896+
"title": "A股收评:指数放量反弹",
897+
"snippet": "科技成长方向领涨",
898+
"source": "测试来源",
899+
"published_date": "2026-05-06",
900+
"url": long_url,
901+
}
902+
],
903+
)
904+
905+
assert long_url not in prompt
906+
assert "URL: https://example.com/redirect?" in prompt
907+
assert ("x" * 220) not in prompt
908+
843909
def test_market_light_snapshot_marks_defensive_market_red(self):
844910
from src.market_analyzer import MarketIndex, MarketOverview
845911

0 commit comments

Comments
 (0)