feat: 首推与昨日冲突时改推并保证一月不重复

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 10:39:12 +08:00
parent d448002e7a
commit 54f164cdbf
3 changed files with 658 additions and 1 deletions

536
daily/featured_pick.py Normal file
View File

@@ -0,0 +1,536 @@
"""今日首推:解析 DAILY_FEATURED_PICK → 定人(月去重)→ LLM 检索 → featured JSON。"""
from __future__ import annotations
import hashlib
import json
import logging
import random
import re
from datetime import datetime, timedelta
from typing import Any
from daily.config import OUTPUT_DIR, ROOT, env, featured_dedup_days
from daily.llm_client import extract_json_object, has_llm_configured, llm_chat
from daily.report_data import featured_json_path, save_json
logger = logging.getLogger(__name__)
_SKILL_DIR = ROOT / "skills" / "daily-featured-pick"
_GITHUB_REPO_RE = re.compile(r"github\.com/([^/\s#?]+/[^/\s#?]+)", re.I)
def featured_identity_key(featured: dict[str, Any] | None) -> str:
"""稳定身份skill→idgithub→repo兜底从 url 解析。"""
if not featured:
return ""
typ = str(featured.get("type") or "").lower()
if typ == "skill" or featured.get("id"):
sid = str(featured.get("id") or "").strip()
if sid:
return sid
repo = str(featured.get("repo") or "").strip()
if repo:
return repo
for field in ("url", "command", "link"):
url = str(featured.get(field) or "")
m = _GITHUB_REPO_RE.search(url)
if m:
return m.group(1)
return ""
def load_recent_featured_keys(date_str: str, days: int | None = None) -> set[str]:
"""近 N 日 data.featured_pick_key 并集(不含当日)。"""
lookback = days if days is not None else featured_dedup_days()
try:
dt = datetime.strptime(date_str, "%Y-%m-%d")
except ValueError:
return set()
out: set[str] = set()
for day_offset in range(1, lookback + 1):
prev = (dt - timedelta(days=day_offset)).strftime("%Y-%m-%d")
path = OUTPUT_DIR / f"{prev}.data.json"
if not path.exists():
continue
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError) as exc:
logger.warning("读取 featured_pick_key %s 失败:%s", path, exc)
continue
data = payload.get("data") if isinstance(payload, dict) else None
if not isinstance(data, dict):
continue
key = str(data.get("featured_pick_key") or "").strip()
if key:
out.add(key)
return out
def load_yesterday_featured_key(date_str: str) -> str | None:
try:
dt = datetime.strptime(date_str, "%Y-%m-%d")
except ValueError:
return None
prev = (dt - timedelta(days=1)).strftime("%Y-%m-%d")
path = OUTPUT_DIR / f"{prev}.data.json"
if not path.exists():
return None
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
data = payload.get("data") if isinstance(payload, dict) else None
if not isinstance(data, dict):
return None
key = str(data.get("featured_pick_key") or "").strip()
return key or None
def _featured_rng(date_str: str) -> random.Random:
seed = int(hashlib.sha256(f"{date_str}:featured".encode()).hexdigest()[:16], 16)
return random.Random(seed)
def _stub_from_pool_item(item: dict[str, Any]) -> dict[str, Any]:
repo = str(item.get("repo") or "").strip()
if repo:
url = str(item.get("url") or f"https://github.com/{repo}").strip()
return {
"type": "github",
"title": repo.split("/")[-1],
"repo": repo,
"url": url,
"command": url,
"summary": str(item.get("description") or "")[:160],
"why_today": "",
"evidence": [],
"tags": [],
}
sid = str(item.get("id") or "").strip()
source = str(item.get("source") or "").strip()
title = str(item.get("title") or "").strip()
return {
"type": "skill",
"id": sid,
"title": title or sid,
"command": _skill_command(item),
"url": str(item.get("link") or ""),
"summary": str(item.get("description") or "")[:160],
"why_today": "",
"evidence": [],
"tags": [],
"source": source,
}
def featured_resolve(
*,
date_str: str,
candidate: dict[str, Any] | None,
pool_a: list[dict[str, Any]],
pool_b: list[dict[str, Any]],
recent_featured: set[str] | None = None,
yesterday_key: str | None = None,
rng: random.Random | None = None,
) -> tuple[dict[str, Any] | None, str | None]:
"""若与昨日同一身份则改推;返回 (seed_stub, identity_key),不含完整 why。"""
if not candidate:
return None, None
key = featured_identity_key(candidate)
if not yesterday_key or key != yesterday_key:
return candidate, key or None
blocked = set(recent_featured or set()) | {yesterday_key}
picker = rng or _featured_rng(date_str)
def _choices(pool: list[dict[str, Any]]) -> list[tuple[str, dict[str, Any]]]:
out: list[tuple[str, dict[str, Any]]] = []
seen: set[str] = set()
for item in pool:
ik = featured_identity_key(item)
if not ik or ik in blocked or ik in seen:
continue
seen.add(ik)
out.append((ik, item))
return out
options = _choices(pool_a)
if not options:
options = _choices(pool_b)
if not options:
logger.info("featured_fallback_exhausted")
return candidate, key
chosen_key, chosen_item = picker.choice(options)
return _stub_from_pool_item(chosen_item), chosen_key
def _config_from_candidate(candidate: dict[str, Any]) -> dict[str, str]:
typ = str(candidate.get("type") or "").lower()
if typ == "skill" or candidate.get("id"):
query = str(candidate.get("id") or candidate.get("title") or "").strip()
return {"query": query, "url_hint": str(candidate.get("url") or "")}
repo = str(candidate.get("repo") or "").strip()
if repo:
return {
"query": repo,
"url_hint": str(candidate.get("url") or f"https://github.com/{repo}"),
}
query = str(candidate.get("title") or candidate.get("url") or "").strip()
return {"query": query or "featured", "url_hint": str(candidate.get("url") or "")}
def _seed_candidate_from_config(
config: dict[str, str],
llm_input: dict[str, Any],
) -> dict[str, Any]:
matches = match_in_data(llm_input, config["query"])
if matches["skills"]:
return _stub_from_pool_item(matches["skills"][0])
if matches["github"]:
return _stub_from_pool_item(matches["github"][0])
url = config.get("url_hint") or ""
seed: dict[str, Any] = {
"type": "other",
"title": config["query"],
"url": url,
"command": url or config["query"],
}
m = _GITHUB_REPO_RE.search(url)
if m:
seed["type"] = "github"
seed["repo"] = m.group(1)
return seed
def parse_featured_pick() -> dict[str, str] | None:
"""解析 DAILY_FEATURED_PICKquery 或 query|url。"""
raw = (env("DAILY_FEATURED_PICK") or "").strip()
if not raw:
return None
if "|" in raw:
query, url_hint = raw.split("|", 1)
query = query.strip()
url_hint = url_hint.strip()
if not query:
return None
payload: dict[str, str] = {"query": query}
if url_hint:
payload["url_hint"] = url_hint
return payload
return {"query": raw}
def _matches_query(text: str, query: str) -> bool:
return query.lower() in (text or "").lower()
def _skill_matches(item: dict[str, Any], query: str) -> bool:
for key in ("id", "title", "source"):
if _matches_query(str(item.get(key) or ""), query):
return True
for sub in item.get("cluster_skills") or []:
if isinstance(sub, str) and _matches_query(sub, query):
return True
return False
def match_in_data(llm_input: dict[str, Any], query: str) -> dict[str, list[dict[str, Any]]]:
"""在榜单数据中模糊匹配 query。"""
skills: list[dict[str, Any]] = []
seen_skill: set[str] = set()
for board in ("skills_trending", "skills_hot"):
for item in llm_input.get(board) or []:
sid = str(item.get("id") or "")
if sid in seen_skill:
continue
if _skill_matches(item, query):
seen_skill.add(sid)
skills.append({**item, "board": board})
if len(skills) >= 5:
break
if len(skills) >= 5:
break
github: list[dict[str, Any]] = []
seen_repo: set[str] = set()
for board in ("github_trending", "github_emerging"):
for item in llm_input.get(board) or []:
repo = str(item.get("repo") or "")
if not repo or repo in seen_repo:
continue
if _matches_query(repo, query):
seen_repo.add(repo)
github.append({**item, "board": board})
if len(github) >= 5:
break
topic = llm_input.get("github_topic") or {}
for item in topic.get("repos") or []:
repo = str(item.get("repo") or "")
if not repo or repo in seen_repo:
continue
if _matches_query(repo, query):
seen_repo.add(repo)
github.append({**item, "board": "github_topic"})
if len(github) >= 5:
break
return {"skills": skills, "github": github}
def _load_skill() -> str:
path = _SKILL_DIR / "SKILL.md"
if path.exists():
return path.read_text(encoding="utf-8").strip()
return "你是早报编辑。根据输入检索今日首推信息,只输出 JSON。"
def _skill_command(item: dict[str, Any]) -> str:
source = str(item.get("source") or "").strip()
title = str(item.get("title") or "").strip()
if source and title:
return f"npx skills add {source}/{title}"
sid = str(item.get("id") or "").strip()
if sid.count("/") >= 2:
parts = sid.split("/", 2)
return f"npx skills add {parts[0]}/{parts[1]}/{parts[2]}"
if sid.count("/") == 1:
return f"npx skills add {sid}"
return ""
def _evidence_from_skill(item: dict[str, Any]) -> list[str]:
board = item.get("board", "")
board_label = {
"skills_trending": "Skills Trending",
"skills_hot": "Skills Hot",
}.get(str(board), str(board))
installs = item.get("installs_fmt") or item.get("installs")
title = item.get("title") or item.get("id") or "?"
if installs:
return [f"{board_label} 匹配 · {title} · {installs}"]
return [f"{board_label} 匹配 · {title}"]
def _evidence_from_github(item: dict[str, Any]) -> list[str]:
board = item.get("board", "")
board_label = {
"github_trending": "GitHub Trending",
"github_emerging": "GitHub 新兴",
"github_topic": "GitHub Topic",
}.get(str(board), str(board))
repo = item.get("repo") or "?"
stars = item.get("total_stars_fmt") or ""
if stars:
return [f"{board_label} 匹配 · {repo} · ⭐{stars}"]
return [f"{board_label} 匹配 · {repo}"]
def _fallback_featured(
config: dict[str, str],
llm_input: dict[str, Any],
*,
partial: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""LLM 不可用或失败时,用榜单匹配 + 配置回退。"""
partial = partial or {}
matches = match_in_data(llm_input, config["query"])
skill = matches["skills"][0] if matches["skills"] else None
gh = matches["github"][0] if matches["github"] else None
if skill:
featured: dict[str, Any] = {
"title": str(skill.get("title") or config["query"]),
"type": "skill",
"command": _skill_command(skill),
"url": str(skill.get("link") or config.get("url_hint") or ""),
"summary": str(partial.get("summary") or skill.get("description") or "")[:160],
"why_today": str(
partial.get("why_today")
or f"今日 Skills 榜匹配到 **{skill.get('title') or config['query']}**,适合作为首推。"
),
"evidence": list(partial.get("evidence") or _evidence_from_skill(skill)),
"tags": list(partial.get("tags") or []),
}
if skill.get("id"):
featured["id"] = skill["id"]
return featured
if gh:
return {
"title": str(gh.get("repo") or config["query"]).split("/")[-1],
"type": "github",
"command": str(gh.get("url") or config.get("url_hint") or ""),
"url": str(gh.get("url") or config.get("url_hint") or ""),
"summary": str(partial.get("summary") or gh.get("description") or "")[:160],
"why_today": str(
partial.get("why_today")
or f"今日 GitHub 榜匹配到 **{gh.get('repo')}**,适合作为首推。"
),
"evidence": list(partial.get("evidence") or _evidence_from_github(gh)),
"tags": list(partial.get("tags") or []),
"repo": gh.get("repo"),
}
url = config.get("url_hint") or ""
return {
"title": config["query"],
"type": "other",
"command": url or config["query"],
"url": url,
"summary": str(partial.get("summary") or "")[:160],
"why_today": str(
partial.get("why_today")
or f"**{config['query']}** 未出现在今日 Top 榜,仍值得单独关注。"
),
"evidence": list(partial.get("evidence") or ([f"主推 · {config['query']}"])),
"tags": list(partial.get("tags") or []),
}
def _normalize_featured(
raw: dict[str, Any],
config: dict[str, str],
llm_input: dict[str, Any],
) -> dict[str, Any]:
"""补齐 command / url / evidence并与榜单数字对齐。"""
matches = match_in_data(llm_input, config["query"])
skill = matches["skills"][0] if matches["skills"] else None
gh = matches["github"][0] if matches["github"] else None
featured = dict(raw)
featured.setdefault("title", config["query"])
featured.setdefault("type", "other")
if skill and featured.get("type") in {"skill", "other", ""}:
featured.setdefault("id", skill.get("id"))
featured.setdefault("command", _skill_command(skill))
featured.setdefault("url", skill.get("link") or config.get("url_hint") or "")
if not featured.get("evidence"):
featured["evidence"] = _evidence_from_skill(skill)
featured["type"] = "skill"
elif gh and featured.get("type") in {"github", "other", ""}:
featured.setdefault("repo", gh.get("repo"))
featured.setdefault("url", gh.get("url") or config.get("url_hint") or "")
featured.setdefault("command", featured.get("url") or gh.get("url") or "")
if not featured.get("evidence"):
featured["evidence"] = _evidence_from_github(gh)
featured["type"] = "github"
featured.setdefault("command", config.get("url_hint") or config["query"])
featured.setdefault("url", config.get("url_hint") or "")
featured.setdefault("summary", "")
featured.setdefault("why_today", featured.get("summary") or "")
featured.setdefault("evidence", [])
featured.setdefault("tags", [])
return featured
def research_featured_pick(
llm_input: dict[str, Any],
*,
date_str: str,
config: dict[str, str] | None = None,
) -> dict[str, Any] | None:
"""Step 0检索今日首推成功返回 featured dict未配置返回 None。"""
config = config or parse_featured_pick()
if not config:
return None
matches = match_in_data(llm_input, config["query"])
payload = {
"query": config["query"],
"url_hint": config.get("url_hint"),
"cwd": env("DAILY_CURSOR_CWD") or str(ROOT),
"data_matches": matches,
}
if not has_llm_configured():
featured = _fallback_featured(config, llm_input)
save_json(featured_json_path(date_str), featured)
logger.info("Featured pick无 LLM规则回退%s", featured.get("title"))
return featured
skill = _load_skill()
system = (
f"{skill}\n\n"
"当前执行 **Step 0今日首推检索**。\n"
"只输出 featured JSONtitle, type, command, url, summary, why_today, evidence, tags"
"不要 Markdown不要解释。"
)
user = json.dumps(payload, ensure_ascii=False, indent=2)
try:
raw = llm_chat(system, user)
except Exception as exc:
logger.warning("Featured pick LLM 失败,回退规则模式:%s", exc)
featured = _fallback_featured(config, llm_input)
save_json(featured_json_path(date_str), featured)
return featured
if not raw:
featured = _fallback_featured(config, llm_input)
save_json(featured_json_path(date_str), featured)
return featured
parsed = extract_json_object(raw)
if not parsed.get("why_today") and not parsed.get("summary"):
logger.warning("Featured pick JSON 无效,回退规则模式")
featured = _fallback_featured(config, llm_input, partial=parsed)
save_json(featured_json_path(date_str), featured)
return featured
featured = _normalize_featured(parsed, config, llm_input)
save_json(featured_json_path(date_str), featured)
logger.info("Featured pick 完成:%s", featured.get("title"))
return featured
def apply_featured_pick(
llm_input: dict[str, Any],
*,
date_str: str,
pool_a: list[dict[str, Any]] | None = None,
pool_b: list[dict[str, Any]] | None = None,
) -> dict[str, Any] | None:
"""先定人(相对昨日改推 + 月去重),再 research写入 featured_pick / featured_pick_key。"""
config = parse_featured_pick()
if not config:
return None
seed = _seed_candidate_from_config(config, llm_input)
recent = load_recent_featured_keys(date_str)
yesterday = load_yesterday_featured_key(date_str)
resolved, identity_key = featured_resolve(
date_str=date_str,
candidate=seed,
pool_a=pool_a or [],
pool_b=pool_b or [],
recent_featured=recent,
yesterday_key=yesterday,
rng=_featured_rng(date_str),
)
research_config = config
if resolved and identity_key and featured_identity_key(seed) != identity_key:
research_config = _config_from_candidate(resolved)
featured = research_featured_pick(
llm_input, date_str=date_str, config=research_config
)
if featured:
llm_input["featured_pick"] = featured
key = identity_key or featured_identity_key(featured)
if key:
llm_input["featured_pick_key"] = key
return featured
def pick_command_from_featured(featured: dict[str, Any] | None) -> str | None:
cmd = str((featured or {}).get("command") or "").strip()
return cmd or None
def pick_why_from_featured(featured: dict[str, Any] | None) -> str | None:
why = str((featured or {}).get("why_today") or "").strip()
return why or None

View File

@@ -50,6 +50,7 @@ from daily.format_wecom import (
from daily.agent_workflow import is_agent_mode, run_agent_workflow from daily.agent_workflow import is_agent_mode, run_agent_workflow
from daily.featured_pick import ( from daily.featured_pick import (
apply_featured_pick, apply_featured_pick,
featured_identity_key,
pick_command_from_featured, pick_command_from_featured,
pick_why_from_featured, pick_why_from_featured,
) )
@@ -638,7 +639,44 @@ def generate_report() -> tuple[str, str, Path, Path]:
research_tech_items=wecom_tech_news if news_merged else None, research_tech_items=wecom_tech_news if news_merged else None,
boards_for_wecom=boards_for_wecom, boards_for_wecom=boards_for_wecom,
) )
featured = apply_featured_pick(llm_input, date_str=date_str) pool_a: list[dict[str, Any]] = []
pool_a_keys: set[str] = set()
for board_name, items in boards_for_wecom.items():
for item in items:
keyed = dict(item)
keyed["board"] = board_name
pool_a.append(keyed)
ik = featured_identity_key(keyed)
if ik:
pool_a_keys.add(ik)
pool_b: list[dict[str, Any]] = []
for board_name, raw_items, kind in (
("skills_trending", trending, "skill"),
("skills_hot", hot, "skill"),
("github_trending", github_trending, "github"),
("github_emerging", github_emerging, "github"),
("github_topic", github_topic, "github"),
):
deep = board_select(
board=board_name,
items=raw_items,
recent_keys=set(),
limit=pool,
pool_size=pool,
kind=kind, # type: ignore[arg-type]
)
for item in deep:
keyed = dict(item)
keyed["board"] = board_name
ik = featured_identity_key(keyed)
if ik and ik not in pool_a_keys:
pool_b.append(keyed)
featured = apply_featured_pick(
llm_input,
date_str=date_str,
pool_a=pool_a,
pool_b=pool_b,
)
movement = llm_input["movement"] movement = llm_input["movement"]
eff_mode = llm_input["effective_wecom_mode"] eff_mode = llm_input["effective_wecom_mode"]
if news_merged: if news_merged:

View File

@@ -0,0 +1,83 @@
# tests/test_featured_resolve.py
from __future__ import annotations
import random
import unittest
from daily.featured_pick import featured_identity_key, featured_resolve
class FeaturedResolveTests(unittest.TestCase):
def test_same_as_yesterday_picks_from_pool_a(self):
yesterday_key = "headroomlabs-ai/headroom"
pool_a = [
{"repo": "headroomlabs-ai/headroom", "board": "github_topic"},
{"repo": "ollama/ollama", "board": "github_trending"},
]
rng = random.Random(0)
resolved, key = featured_resolve(
date_str="2026-07-14",
candidate={
"type": "github",
"url": "https://github.com/headroomlabs-ai/headroom",
"title": "headroom",
},
pool_a=pool_a,
pool_b=[],
recent_featured={yesterday_key},
yesterday_key=yesterday_key,
rng=rng,
)
self.assertNotEqual(key, yesterday_key)
self.assertEqual(key, "ollama/ollama")
self.assertIsNotNone(resolved)
assert resolved is not None
self.assertEqual(resolved.get("repo"), "ollama/ollama")
def test_pool_a_before_pool_b(self):
yesterday_key = "blocked/one"
pool_a = [{"repo": "pool-a/repo", "board": "github_trending"}]
pool_b = [{"repo": "pool-b/repo", "board": "github_emerging"}]
resolved, key = featured_resolve(
date_str="2026-07-14",
candidate={"type": "github", "repo": "blocked/one", "title": "one"},
pool_a=pool_a,
pool_b=pool_b,
recent_featured={yesterday_key},
yesterday_key=yesterday_key,
rng=random.Random(1),
)
self.assertEqual(key, "pool-a/repo")
assert resolved is not None
self.assertEqual(resolved.get("repo"), "pool-a/repo")
def test_exhausted_keeps_original(self):
yesterday_key = "only/one"
candidate = {"type": "github", "repo": "only/one", "title": "one"}
resolved, key = featured_resolve(
date_str="2026-07-14",
candidate=candidate,
pool_a=[{"repo": "only/one", "board": "github_trending"}],
pool_b=[],
recent_featured={yesterday_key},
yesterday_key=yesterday_key,
rng=random.Random(2),
)
self.assertEqual(key, yesterday_key)
self.assertEqual(resolved, candidate)
def test_identity_key_skill_and_github(self):
self.assertEqual(
featured_identity_key({"type": "skill", "id": "a/b/c"}),
"a/b/c",
)
self.assertEqual(
featured_identity_key(
{"type": "github", "url": "https://github.com/foo/bar"}
),
"foo/bar",
)
if __name__ == "__main__":
unittest.main()