From 0c324f9ace587fdaa4e3f1e4463c5f37d3badd91 Mon Sep 17 00:00:00 2001 From: yumao Date: Tue, 14 Jul 2026 10:32:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=20board=5Fselect=20?= =?UTF-8?q?=E5=91=A8=E5=8E=BB=E9=87=8D=E4=B8=8E=E6=B7=B1=E6=B1=A0=E8=A1=A5?= =?UTF-8?q?=E6=BB=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- daily/board_select.py | 46 ++++++++++++++++++++++++++++++ tests/test_board_select.py | 58 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 daily/board_select.py create mode 100644 tests/test_board_select.py diff --git a/daily/board_select.py b/daily/board_select.py new file mode 100644 index 0000000..b0151bd --- /dev/null +++ b/daily/board_select.py @@ -0,0 +1,46 @@ +"""五榜唯一列表主人:周去重 + 深池补满。""" + +from __future__ import annotations + +import logging +from typing import Any, Literal + +from daily.delta import skill_id + +logger = logging.getLogger(__name__) + + +def board_select( + *, + board: str, + items: list[dict[str, Any]], + recent_keys: set[str], + limit: int, + pool_size: int, + kind: Literal["skill", "github"], +) -> list[dict[str, Any]]: + """从深池过滤近 N 日已展示 key,按原顺序取满 limit;不足则短榜。""" + if kind == "skill": + from daily.skills_group import group_skills_by_source + + pool = group_skills_by_source(items, limit=pool_size, pool_size=pool_size) + + def key_fn(x: dict[str, Any]) -> str: + return skill_id(x) + else: + pool = items[: max(pool_size, limit)] + + def key_fn(x: dict[str, Any]) -> str: + return str(x.get("repo") or "") + + out: list[dict[str, Any]] = [] + for item in pool: + k = key_fn(item) + if not k or k in recent_keys: + continue + out.append(item) + if len(out) >= limit: + break + if len(out) < limit: + logger.info("board_short:%s:%s", board, len(out)) + return out diff --git a/tests/test_board_select.py b/tests/test_board_select.py new file mode 100644 index 0000000..779e472 --- /dev/null +++ b/tests/test_board_select.py @@ -0,0 +1,58 @@ +# tests/test_board_select.py +from __future__ import annotations + +import unittest + +from daily.board_select import board_select + + +def _gh(repo: str) -> dict: + return {"repo": repo, "description": repo} + + +class BoardSelectTests(unittest.TestCase): + def test_filters_recent_and_keeps_order(self): + pool = [_gh(f"o/r{i}") for i in range(20)] + recent = {"o/r0", "o/r1", "o/r2"} + out = board_select( + board="github_trending", + items=pool, + recent_keys=recent, + limit=5, + pool_size=20, + kind="github", + ) + keys = [x["repo"] for x in out] + self.assertEqual(keys, ["o/r3", "o/r4", "o/r5", "o/r6", "o/r7"]) + + def test_deep_pool_fills_after_filter(self): + pool = [_gh(f"o/r{i}") for i in range(8)] + recent = {f"o/r{i}" for i in range(6)} # 前 6 全封 + out = board_select( + board="github_emerging", + items=pool, + recent_keys=recent, + limit=5, + pool_size=8, + kind="github", + ) + self.assertEqual([x["repo"] for x in out], ["o/r6", "o/r7"]) # 短榜 + + def test_skill_uses_skill_id(self): + items = [ + {"id": "a/b/s1", "source": "a/b", "title": "s1", "installs": 10}, + {"id": "c/d/s2", "source": "c/d", "title": "s2", "installs": 9}, + ] + out = board_select( + board="skills_trending", + items=items, + recent_keys={"a/b/s1"}, + limit=10, + pool_size=50, + kind="skill", + ) + self.assertEqual([x["id"] for x in out], ["c/d/s2"]) + + +if __name__ == "__main__": + unittest.main()