feat: theme_line 取数升级,无评分命中时回退 theme_names
新增 _top_line: 优先评分最高主题,无命中时回退 _theme_clusters 的 主题名(非 markdown 示例),DAILY_WECOM_TOP_LINE=0 退回原逻辑。 复用现有 theme_line 行,不新增头部行(T5)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ from daily.config import (
|
|||||||
SNAPSHOT_FILE,
|
SNAPSHOT_FILE,
|
||||||
board_pool_size,
|
board_pool_size,
|
||||||
env,
|
env,
|
||||||
|
env_bool,
|
||||||
env_int,
|
env_int,
|
||||||
full_desc_limit,
|
full_desc_limit,
|
||||||
news_summary_limit,
|
news_summary_limit,
|
||||||
@@ -345,6 +346,33 @@ def _detect_theme_line(feed: dict[str, Any]) -> str:
|
|||||||
return f"**今日主题**:{max(scores.items(), key=lambda x: x[1])[0]}"
|
return f"**今日主题**:{max(scores.items(), key=lambda x: x[1])[0]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _top_line(feed: dict[str, Any]) -> str:
|
||||||
|
"""今日看点行: 优先评分最高的主题, 回退 _theme_clusters 的主题名(非 markdown 示例)。
|
||||||
|
|
||||||
|
DAILY_WECOM_TOP_LINE=0 关闭时退回 _detect_theme_line 原逻辑。
|
||||||
|
与原 _detect_theme_line 的差别仅在「无评分命中」时的兜底文案:
|
||||||
|
用 theme_names 的真实主题取代硬编码「Agent Skills 生态持续活跃」。
|
||||||
|
"""
|
||||||
|
if not env_bool("DAILY_WECOM_TOP_LINE", True):
|
||||||
|
return _detect_theme_line(feed)
|
||||||
|
scores: dict[str, int] = defaultdict(int)
|
||||||
|
for board in ("topTrending", "topHot"):
|
||||||
|
for rank, item in enumerate(feed.get(board, [])[:10], 1):
|
||||||
|
haystack = " ".join(
|
||||||
|
[item.get("title", ""), item.get("source", ""), item.get("description", "")]
|
||||||
|
).lower()
|
||||||
|
for _icon, label, keywords in THEME_RULES:
|
||||||
|
if any(k in haystack for k in keywords):
|
||||||
|
scores[label] += max(1, 11 - rank)
|
||||||
|
break
|
||||||
|
if scores:
|
||||||
|
return f"**今日主题**:{max(scores.items(), key=lambda x: x[1])[0]}"
|
||||||
|
names = theme_names(feed, theme_rules=THEME_RULES, skill_id_fn=_skill_id)
|
||||||
|
if names:
|
||||||
|
return f"**今日主题**:{' · '.join(names)}"
|
||||||
|
return "**今日主题**:Agent Skills 生态持续活跃"
|
||||||
|
|
||||||
|
|
||||||
def _build_highlights(
|
def _build_highlights(
|
||||||
trending: list[dict[str, Any]],
|
trending: list[dict[str, Any]],
|
||||||
hot: list[dict[str, Any]],
|
hot: list[dict[str, Any]],
|
||||||
@@ -1076,7 +1104,7 @@ def _render(c: dict[str, Any], s: dict[str, Any]) -> tuple[str, str, Path, Path]
|
|||||||
updated=updated,
|
updated=updated,
|
||||||
highlights=editorial_highlights
|
highlights=editorial_highlights
|
||||||
or _build_highlights(trending, hot, github_trending, github_emerging, ai_news, cn_ai_news),
|
or _build_highlights(trending, hot, github_trending, github_emerging, ai_news, cn_ai_news),
|
||||||
theme_line=editorial_theme or _detect_theme_line(feed),
|
theme_line=editorial_theme or _top_line(feed),
|
||||||
ai_news=wecom_ai if not news_merged else None,
|
ai_news=wecom_ai if not news_merged else None,
|
||||||
cn_ai_news=wecom_cn if not news_merged else None,
|
cn_ai_news=wecom_cn if not news_merged else None,
|
||||||
merged_ai_news=wecom_news if news_merged else None,
|
merged_ai_news=wecom_news if news_merged else None,
|
||||||
|
|||||||
60
tests/test_top_line.py
Normal file
60
tests/test_top_line.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# tests/test_top_line.py
|
||||||
|
"""T5: _top_line 看点行(theme_line 取数升级)测试。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import daily.generate as g
|
||||||
|
|
||||||
|
FEED_HIT = {
|
||||||
|
"topTrending": [
|
||||||
|
{"title": "remotion-video", "source": "src", "description": "video tool"},
|
||||||
|
],
|
||||||
|
"topHot": [],
|
||||||
|
}
|
||||||
|
FEED_MISS = {
|
||||||
|
"topTrending": [
|
||||||
|
{"title": "zzz-nomatch", "source": "src", "description": "nothing"},
|
||||||
|
],
|
||||||
|
"topHot": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TopLineTests(unittest.TestCase):
|
||||||
|
def test_scores_hit_returns_theme(self):
|
||||||
|
# 命中 THEME_RULES(video) -> 评分最高主题
|
||||||
|
with mock.patch.object(g, "env_bool", return_value=True):
|
||||||
|
line = g._top_line(FEED_HIT)
|
||||||
|
self.assertIn("今日主题", line)
|
||||||
|
self.assertIn("AI 多媒体", line)
|
||||||
|
|
||||||
|
def test_fallback_to_theme_names_when_no_score(self):
|
||||||
|
# 无评分命中但 theme_clusters 能聚类 -> 用主题名
|
||||||
|
feed = {
|
||||||
|
"topTrending": [
|
||||||
|
{"id": "x", "title": "runcomfy-x", "source": "s", "description": "video x"},
|
||||||
|
],
|
||||||
|
"topHot": [],
|
||||||
|
}
|
||||||
|
with mock.patch.object(g, "env_bool", return_value=True):
|
||||||
|
# 让评分落空(前 10 无命中)但 clusters(前 20)命中
|
||||||
|
line = g._top_line(feed)
|
||||||
|
self.assertIn("今日主题", line)
|
||||||
|
self.assertIn("AI 多媒体", line)
|
||||||
|
|
||||||
|
def test_switch_off_uses_legacy_detect(self):
|
||||||
|
# DAILY_WECOM_TOP_LINE=0 -> 退回 _detect_theme_line
|
||||||
|
with mock.patch.object(g, "env_bool", return_value=False):
|
||||||
|
line = g._top_line(FEED_MISS)
|
||||||
|
self.assertEqual(line, g._detect_theme_line(FEED_MISS))
|
||||||
|
|
||||||
|
def test_empty_feed_hardcoded_fallback(self):
|
||||||
|
# 完全无数据 -> 硬编码兜底, 不抛异常
|
||||||
|
with mock.patch.object(g, "env_bool", return_value=True):
|
||||||
|
line = g._top_line({"topTrending": [], "topHot": []})
|
||||||
|
self.assertIn("今日主题", line)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user