From 33bfed0e79c9a1370db63922fc4ab3fa012fec7b Mon Sep 17 00:00:00 2001 From: yumao Date: Sat, 18 Jul 2026 16:44:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20theme=5Fline=20=E5=8F=96=E6=95=B0?= =?UTF-8?q?=E5=8D=87=E7=BA=A7,=E6=97=A0=E8=AF=84=E5=88=86=E5=91=BD?= =?UTF-8?q?=E4=B8=AD=E6=97=B6=E5=9B=9E=E9=80=80=20theme=5Fnames?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 _top_line: 优先评分最高主题,无命中时回退 _theme_clusters 的 主题名(非 markdown 示例),DAILY_WECOM_TOP_LINE=0 退回原逻辑。 复用现有 theme_line 行,不新增头部行(T5)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- daily/generate.py | 30 ++++++++++++++++++++- tests/test_top_line.py | 60 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 tests/test_top_line.py diff --git a/daily/generate.py b/daily/generate.py index dc3ecf6..5c5b888 100644 --- a/daily/generate.py +++ b/daily/generate.py @@ -24,6 +24,7 @@ from daily.config import ( SNAPSHOT_FILE, board_pool_size, env, + env_bool, env_int, full_desc_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]}" +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( trending: 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, highlights=editorial_highlights 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, cn_ai_news=wecom_cn if not news_merged else None, merged_ai_news=wecom_news if news_merged else None, diff --git a/tests/test_top_line.py b/tests/test_top_line.py new file mode 100644 index 0000000..d993a27 --- /dev/null +++ b/tests/test_top_line.py @@ -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()