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:
2026-07-18 16:44:15 +08:00
parent ea8de9fe61
commit 33bfed0e79
2 changed files with 89 additions and 1 deletions

View File

@@ -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,

60
tests/test_top_line.py Normal file
View 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()