feat: Phase 2 新闻打分去重与 Agent 输入池裁剪
标题相似度合并、信源/时效/昨日重复加权排序,Agent 模式扩大 LLM 新闻候选池并记录抓取失败源。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -286,7 +286,7 @@ def _fetch_one(
|
||||
client: httpx.Client,
|
||||
category: NewsCategory,
|
||||
feed: NewsFeed,
|
||||
) -> list[dict[str, Any]]:
|
||||
) -> tuple[list[dict[str, Any]], bool]:
|
||||
last_exc: Exception | None = None
|
||||
for url in _reddit_fetch_urls(feed.url):
|
||||
try:
|
||||
@@ -294,12 +294,12 @@ def _fetch_one(
|
||||
resp = client.get(url, headers=headers)
|
||||
resp.raise_for_status()
|
||||
entries = _parse_feed(resp.text, feed.name, category)
|
||||
return _filter_ai_entries(entries, ai_filter=feed.ai_filter)
|
||||
return _filter_ai_entries(entries, ai_filter=feed.ai_filter), True
|
||||
except Exception as exc:
|
||||
last_exc = exc
|
||||
continue
|
||||
logger.warning("RSS fetch failed [%s] %s: %s", feed.name, feed.url, last_exc)
|
||||
return []
|
||||
return [], False
|
||||
|
||||
|
||||
def _dedupe_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
@@ -344,7 +344,12 @@ def _fetch_news(categories: tuple[NewsCategory, ...]) -> dict[str, Any]:
|
||||
tasks.append((category, feed))
|
||||
|
||||
raw_by_category: dict[str, list[dict[str, Any]]] = {c.id: [] for c in categories}
|
||||
stats = {"feeds_total": len(tasks), "feeds_ok": 0, "items_raw": 0}
|
||||
stats: dict[str, Any] = {
|
||||
"feeds_total": len(tasks),
|
||||
"feeds_ok": 0,
|
||||
"items_raw": 0,
|
||||
"feeds_failed": [],
|
||||
}
|
||||
|
||||
with httpx.Client(timeout=15.0, verify=certifi.where(), follow_redirects=True, headers=headers) as client:
|
||||
fast_tasks = [t for t in tasks if not t[1].slow]
|
||||
@@ -358,19 +363,24 @@ def _fetch_news(categories: tuple[NewsCategory, ...]) -> dict[str, Any]:
|
||||
for future in as_completed(futures):
|
||||
cat_id, feed_name = futures[future]
|
||||
try:
|
||||
entries = future.result()
|
||||
entries, ok = future.result()
|
||||
except Exception as exc:
|
||||
logger.warning("RSS 任务异常 [%s]: %s", feed_name, exc)
|
||||
stats["feeds_failed"].append(feed_name)
|
||||
continue
|
||||
if entries:
|
||||
if ok:
|
||||
stats["feeds_ok"] += 1
|
||||
else:
|
||||
stats["feeds_failed"].append(feed_name)
|
||||
stats["items_raw"] += len(entries)
|
||||
raw_by_category[cat_id].extend(entries[:per_feed])
|
||||
|
||||
for cat, feed in slow_tasks:
|
||||
entries = _fetch_one(client, cat, feed)
|
||||
if entries:
|
||||
entries, ok = _fetch_one(client, cat, feed)
|
||||
if ok:
|
||||
stats["feeds_ok"] += 1
|
||||
else:
|
||||
stats["feeds_failed"].append(feed.name)
|
||||
stats["items_raw"] += len(entries)
|
||||
raw_by_category[cat.id].extend(entries[:per_feed])
|
||||
if _is_reddit_url(feed.url):
|
||||
@@ -467,6 +477,12 @@ def _format_news_section(
|
||||
f"> 近 **{hours}h** · {news.get('stats', {}).get('feeds_ok', 0)}/{news.get('stats', {}).get('feeds_total', 0)} 源可用",
|
||||
"",
|
||||
]
|
||||
failed = news.get("stats", {}).get("feeds_failed") or []
|
||||
if failed:
|
||||
preview = "、".join(failed[:5])
|
||||
suffix = "…" if len(failed) > 5 else ""
|
||||
lines.append(f"> ⚠️ {len(failed)} 个源抓取失败:{preview}{suffix}")
|
||||
lines.append("")
|
||||
|
||||
if not categories:
|
||||
lines.append("*暂无可用条目(网络/RSS 源异常或时间窗口内无更新)。*")
|
||||
@@ -497,12 +513,20 @@ def _format_news_section(
|
||||
return lines
|
||||
|
||||
|
||||
def prepare_wecom_news_items(news: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
def _sorted_flat(news: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
flat = list(news.get("flat") or [])
|
||||
flat.sort(
|
||||
key=lambda item: (float(item.get("score") or 0), _sort_key(item)[1]),
|
||||
reverse=True,
|
||||
)
|
||||
return flat
|
||||
|
||||
|
||||
def prepare_wecom_news_items(news: dict[str, Any], *, limit: int | None = None) -> list[dict[str, Any]]:
|
||||
if not news.get("enabled"):
|
||||
return []
|
||||
limit = _wecom_limit()
|
||||
flat = _dedupe_items(news.get("flat") or [])
|
||||
flat.sort(key=_sort_key, reverse=True)
|
||||
pick_limit = limit or _wecom_limit()
|
||||
flat = _sorted_flat(news)
|
||||
preferred = ("media", "newsletter", "official", "community", "research", "developer")
|
||||
picked: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
@@ -513,12 +537,12 @@ def prepare_wecom_news_items(news: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
continue
|
||||
picked.append(item)
|
||||
seen.add(link)
|
||||
if len(picked) >= limit:
|
||||
if len(picked) >= pick_limit:
|
||||
break
|
||||
if len(picked) >= limit:
|
||||
if len(picked) >= pick_limit:
|
||||
break
|
||||
items: list[dict[str, Any]] = []
|
||||
for item in picked[:limit]:
|
||||
for item in picked[:pick_limit]:
|
||||
items.append(
|
||||
{
|
||||
"title": item.get("title", "?"),
|
||||
@@ -526,17 +550,17 @@ def prepare_wecom_news_items(news: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"source_name": item.get("source_name", "?"),
|
||||
"published_fmt": item.get("published_fmt", ""),
|
||||
"desc_short": _clean_text(item.get("summary", ""), 36),
|
||||
"score": item.get("score"),
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
def prepare_wecom_cn_news_items(news: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
def prepare_wecom_cn_news_items(news: dict[str, Any], *, limit: int | None = None) -> list[dict[str, Any]]:
|
||||
if not news.get("enabled"):
|
||||
return []
|
||||
limit = _wecom_cn_limit()
|
||||
flat = _dedupe_items(news.get("flat") or [])
|
||||
flat.sort(key=_sort_key, reverse=True)
|
||||
pick_limit = limit or _wecom_cn_limit()
|
||||
flat = _sorted_flat(news)
|
||||
preferred = ("media", "tech", "dev")
|
||||
picked: list[dict[str, Any]] = []
|
||||
seen_links: set[str] = set()
|
||||
@@ -551,23 +575,23 @@ def prepare_wecom_cn_news_items(news: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
picked.append(item)
|
||||
seen_links.add(link)
|
||||
seen_sources.add(source)
|
||||
if len(picked) >= limit:
|
||||
if len(picked) >= pick_limit:
|
||||
break
|
||||
if len(picked) >= limit:
|
||||
if len(picked) >= pick_limit:
|
||||
break
|
||||
|
||||
if len(picked) < limit:
|
||||
if len(picked) < pick_limit:
|
||||
for item in flat:
|
||||
link = _normalize_link(item.get("link", ""))
|
||||
if not link or link in seen_links:
|
||||
continue
|
||||
picked.append(item)
|
||||
seen_links.add(link)
|
||||
if len(picked) >= limit:
|
||||
if len(picked) >= pick_limit:
|
||||
break
|
||||
|
||||
items: list[dict[str, Any]] = []
|
||||
for item in picked[:limit]:
|
||||
for item in picked[:pick_limit]:
|
||||
items.append(
|
||||
{
|
||||
"title": item.get("title", "?"),
|
||||
@@ -575,6 +599,7 @@ def prepare_wecom_cn_news_items(news: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"source_name": item.get("source_name", "?"),
|
||||
"published_fmt": item.get("published_fmt", ""),
|
||||
"desc_short": _clean_text(item.get("summary", ""), 36),
|
||||
"score": item.get("score"),
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
Reference in New Issue
Block a user