feat: 早报系统重构与功能增强
- 新增常驻调度器 daily/scheduler.py + run-scheduler.ps1(定时生成/推送) - 新增 daily/bridge_manager.py:Windows 兼容的 Cursor SDK 桥接 - 新增 skills/daily-featured-pick 首推 Skill 与叙事轴/去重逻辑 - 新闻抓取窗口、GitHub 搜索、企微 delta 模式等多项改进 - 补充设计文档与 superpowers 计划/规范 - 新增对应测试(scheduler、featured_pick、github_search、news_fetch_window 等) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ from typing import Any
|
||||
|
||||
from daily.config import wecom_skill_desc_limit
|
||||
from daily.localize import LocalizeJob, localize_brief_descriptions, needs_chinese
|
||||
from daily.skills_group import group_skills_by_source, skill_id as board_skill_id
|
||||
from daily.skills_group import group_skills_by_source
|
||||
from daily.text_utils import trim_brief
|
||||
|
||||
ICONS = {
|
||||
@@ -469,6 +469,32 @@ def _prepare_grouped_wecom_skills(
|
||||
return wecom_items[:limit], keys
|
||||
|
||||
|
||||
def _normalize_skill_source_groups(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""将条目规范为按 source 合并的榜单项(展示始终为合并态)。"""
|
||||
flat: list[dict[str, Any]] = []
|
||||
for item in items:
|
||||
flat.extend(_flatten_skill_board_item(item))
|
||||
if not flat:
|
||||
return []
|
||||
return group_skills_by_source(flat, limit=len(flat), pool_size=len(flat))
|
||||
|
||||
|
||||
def _skill_primary_id(item: dict[str, Any]) -> str:
|
||||
return str(item.get("id") or f"{item.get('source')}/{item.get('title')}" or "").strip()
|
||||
|
||||
|
||||
def _source_from_skill_key(key: str) -> str:
|
||||
from daily.skills_group import source_from_skill_key
|
||||
|
||||
return source_from_skill_key(key)
|
||||
|
||||
|
||||
def expand_skill_recent_keys(keys: set[str] | None) -> set[str]:
|
||||
from daily.skills_group import expand_skill_recent_keys as _expand
|
||||
|
||||
return _expand(keys)
|
||||
|
||||
|
||||
def _merge_skill_board_items(
|
||||
moves: list[dict[str, Any]],
|
||||
full_items: list[dict[str, Any]],
|
||||
@@ -477,29 +503,80 @@ def _merge_skill_board_items(
|
||||
exclude_keys: set[str] | None = None,
|
||||
recent_keys: set[str] | None = None,
|
||||
) -> tuple[list[dict[str, Any]], set[str]]:
|
||||
"""异动优先,不足时用深池补满;按 source 合并态取条。
|
||||
|
||||
周去重按 source(含从 skill id 展开);同日避开其它榜时也按 source。
|
||||
"""
|
||||
from daily.delta import skill_id as move_skill_id
|
||||
|
||||
exclude = exclude_keys or set()
|
||||
recent = recent_keys or set()
|
||||
seen: set[str] = set()
|
||||
flat: list[dict[str, Any]] = []
|
||||
recent = expand_skill_recent_keys(recent_keys)
|
||||
exclude_sources = {_source_from_skill_key(k) for k in exclude if k}
|
||||
exclude_sources.update(k for k in exclude if k)
|
||||
|
||||
def _blocked(item: dict[str, Any]) -> bool:
|
||||
primary = _skill_primary_id(item)
|
||||
source = str(item.get("source") or "").strip()
|
||||
if primary and primary in recent:
|
||||
return True
|
||||
if source and source in recent:
|
||||
return True
|
||||
if primary and primary in exclude:
|
||||
return True
|
||||
if source and (source in exclude_sources or source in exclude):
|
||||
return True
|
||||
return False
|
||||
|
||||
groups: list[dict[str, Any]] = []
|
||||
seen_sources: set[str] = set()
|
||||
|
||||
move_rows: list[dict[str, Any]] = []
|
||||
seen_move_ids: set[str] = set()
|
||||
for move in moves:
|
||||
key = move_skill_id(move)
|
||||
if not key or key in seen or key in exclude:
|
||||
if not key or key in seen_move_ids:
|
||||
continue
|
||||
seen.add(key)
|
||||
flat.append(_move_to_skill_row(move))
|
||||
pad_pool = max(limit * 5, len(flat), 50)
|
||||
for item in full_items:
|
||||
if len(flat) >= pad_pool:
|
||||
move_source = str(move.get("source") or "").strip()
|
||||
if key in exclude or (move_source and move_source in exclude_sources):
|
||||
continue
|
||||
if key in recent or (move_source and move_source in recent):
|
||||
continue
|
||||
seen_move_ids.add(key)
|
||||
move_rows.append(_move_to_skill_row(move))
|
||||
|
||||
for group in _normalize_skill_source_groups(move_rows):
|
||||
source = str(group.get("source") or "?")
|
||||
if source in seen_sources or _blocked(group):
|
||||
continue
|
||||
seen_sources.add(source)
|
||||
groups.append(group)
|
||||
if len(groups) >= limit:
|
||||
break
|
||||
for row in _flatten_skill_board_item(item):
|
||||
key = board_skill_id(row)
|
||||
if not key or key in seen or key in exclude or key in recent:
|
||||
|
||||
if len(groups) < limit:
|
||||
for group in _normalize_skill_source_groups(full_items):
|
||||
if len(groups) >= limit:
|
||||
break
|
||||
source = str(group.get("source") or "?")
|
||||
if source in seen_sources or _blocked(group):
|
||||
continue
|
||||
seen.add(key)
|
||||
flat.append(row)
|
||||
return _prepare_grouped_wecom_skills(flat, limit=limit)
|
||||
seen_sources.add(source)
|
||||
groups.append(group)
|
||||
|
||||
if not groups:
|
||||
return [], set()
|
||||
prepared = finalize_wecom_skill_groups(groups[:limit])
|
||||
wecom_items = [_grouped_skill_to_wecom_item(x) for x in prepared]
|
||||
# 供同日 Hot 排除:主键 + source
|
||||
keys: set[str] = set()
|
||||
for item in groups[:limit]:
|
||||
primary = _skill_primary_id(item)
|
||||
if primary:
|
||||
keys.add(primary)
|
||||
source = str(item.get("source") or "").strip()
|
||||
if source:
|
||||
keys.add(source)
|
||||
return wecom_items[:limit], keys
|
||||
|
||||
|
||||
def build_skills_delta_sections(
|
||||
@@ -508,8 +585,8 @@ def build_skills_delta_sections(
|
||||
*,
|
||||
trending_full: list[dict[str, Any]] | None = None,
|
||||
hot_full: list[dict[str, Any]] | None = None,
|
||||
trending_limit: int = 10,
|
||||
hot_limit: int = 10,
|
||||
trending_limit: int = 5,
|
||||
hot_limit: int = 5,
|
||||
pad: bool = False,
|
||||
recent_trending: set[str] | None = None,
|
||||
recent_hot: set[str] | None = None,
|
||||
@@ -517,11 +594,14 @@ def build_skills_delta_sections(
|
||||
sections: list[str] = []
|
||||
trending_keys: set[str] = set()
|
||||
if pad:
|
||||
skill_recent = expand_skill_recent_keys(
|
||||
(recent_trending or set()) | (recent_hot or set())
|
||||
)
|
||||
t_items, trending_keys = _merge_skill_board_items(
|
||||
trending_moves,
|
||||
trending_full or [],
|
||||
trending_limit,
|
||||
recent_keys=recent_trending,
|
||||
recent_keys=skill_recent,
|
||||
)
|
||||
if t_items:
|
||||
lines = [f"{ICONS['trending']} **Skills Trending Top {len(t_items)}**"]
|
||||
@@ -533,7 +613,7 @@ def build_skills_delta_sections(
|
||||
hot_full or [],
|
||||
hot_limit,
|
||||
exclude_keys=trending_keys,
|
||||
recent_keys=recent_hot,
|
||||
recent_keys=skill_recent,
|
||||
)
|
||||
if h_items:
|
||||
lines = [f"{ICONS['hot']} **Skills Hot Top {len(h_items)}**"]
|
||||
@@ -585,7 +665,7 @@ def _merge_github_board_items(
|
||||
for move in moves:
|
||||
repo = _github_move_to_repo(move)
|
||||
key = str(repo.get("repo") or "")
|
||||
if not key or key in seen:
|
||||
if not key or key in seen or key in recent:
|
||||
continue
|
||||
seen.add(key)
|
||||
merged.append(repo)
|
||||
@@ -607,15 +687,20 @@ def build_github_delta_sections(
|
||||
github_trending: list[dict[str, Any]] | None = None,
|
||||
github_emerging: list[dict[str, Any]] | None = None,
|
||||
github_topic: list[dict[str, Any]] | None = None,
|
||||
trending_limit: int = 10,
|
||||
emerging_limit: int = 10,
|
||||
topic_limit: int = 10,
|
||||
trending_limit: int = 5,
|
||||
emerging_limit: int = 5,
|
||||
topic_limit: int = 5,
|
||||
pad: bool = False,
|
||||
recent_board_keys: dict[str, set[str]] | None = None,
|
||||
) -> str:
|
||||
sections: list[str] = []
|
||||
recent = recent_board_keys or {}
|
||||
if pad:
|
||||
github_recent = (
|
||||
(recent.get("github_trending") or set())
|
||||
| (recent.get("github_emerging") or set())
|
||||
| (recent.get("github_topic") or set())
|
||||
)
|
||||
mapping = [
|
||||
("github_trending_moves", "github_trending", github_trending or [], trending_limit, "github", "GitHub Trending", False),
|
||||
("github_emerging_moves", "github_emerging", github_emerging or [], emerging_limit, "emerging", "GitHub 新兴", True),
|
||||
@@ -626,8 +711,9 @@ def build_github_delta_sections(
|
||||
movement.get(move_key) or [],
|
||||
full_repos,
|
||||
limit,
|
||||
recent_repos=recent.get(board_key),
|
||||
recent_repos=github_recent,
|
||||
)
|
||||
github_recent |= {str(r.get("repo") or "") for r in repos if r.get("repo")}
|
||||
if not repos:
|
||||
continue
|
||||
lines = [f"{ICONS[icon_key]} **{label} Top {len(repos)}**"]
|
||||
@@ -678,11 +764,11 @@ def resolve_wecom_board_items(
|
||||
github_trending: list[dict[str, Any]] | None = None,
|
||||
github_emerging: list[dict[str, Any]] | None = None,
|
||||
github_topic: list[dict[str, Any]] | None = None,
|
||||
wecom_trending: int = 10,
|
||||
wecom_hot: int = 10,
|
||||
wecom_github: int = 10,
|
||||
wecom_emerging: int = 10,
|
||||
wecom_topic: int = 10,
|
||||
wecom_trending: int = 5,
|
||||
wecom_hot: int = 5,
|
||||
wecom_github: int = 5,
|
||||
wecom_emerging: int = 5,
|
||||
wecom_topic: int = 5,
|
||||
pad: bool = False,
|
||||
date_str: str | None = None,
|
||||
trending_pad: list[dict[str, Any]] | None = None,
|
||||
@@ -704,8 +790,21 @@ def resolve_wecom_board_items(
|
||||
}
|
||||
|
||||
recent_board_keys: dict[str, set[str]] = {}
|
||||
skill_recent: set[str] = set()
|
||||
github_recent: set[str] = set()
|
||||
if pad and date_str:
|
||||
recent_board_keys = load_recent_board_keys(date_str)
|
||||
# Trending / Hot 共用周去重:任一类出现过的 source 两边都不再展示
|
||||
skill_recent = expand_skill_recent_keys(
|
||||
(recent_board_keys.get("skills_trending") or set())
|
||||
| (recent_board_keys.get("skills_hot") or set())
|
||||
)
|
||||
# GitHub 三榜共用周去重:任一类出现过的 repo 各榜都不再展示
|
||||
github_recent = (
|
||||
(recent_board_keys.get("github_trending") or set())
|
||||
| (recent_board_keys.get("github_emerging") or set())
|
||||
| (recent_board_keys.get("github_topic") or set())
|
||||
)
|
||||
|
||||
t_moves, h_moves = partition_skill_moves_for_wecom(
|
||||
movement.get("skills_trending_moves") or [],
|
||||
@@ -716,36 +815,41 @@ def resolve_wecom_board_items(
|
||||
t_moves,
|
||||
trending_pad if trending_pad else trending,
|
||||
wecom_trending,
|
||||
recent_keys=recent_board_keys.get("skills_trending"),
|
||||
recent_keys=skill_recent,
|
||||
)
|
||||
h_items, _ = _merge_skill_board_items(
|
||||
h_moves,
|
||||
hot_pad if hot_pad else hot,
|
||||
wecom_hot,
|
||||
exclude_keys=trending_keys,
|
||||
recent_keys=recent_board_keys.get("skills_hot"),
|
||||
recent_keys=skill_recent,
|
||||
)
|
||||
gt_items = _merge_github_board_items(
|
||||
movement.get("github_trending_moves") or [],
|
||||
github_trending_pad if github_trending_pad else (github_trending or []),
|
||||
wecom_github,
|
||||
recent_repos=github_recent,
|
||||
)
|
||||
github_recent |= {str(r.get("repo") or "") for r in gt_items if r.get("repo")}
|
||||
ge_items = _merge_github_board_items(
|
||||
movement.get("github_emerging_moves") or [],
|
||||
github_emerging_pad if github_emerging_pad else (github_emerging or []),
|
||||
wecom_emerging,
|
||||
recent_repos=github_recent,
|
||||
)
|
||||
github_recent |= {str(r.get("repo") or "") for r in ge_items if r.get("repo")}
|
||||
gtopic_items = _merge_github_board_items(
|
||||
movement.get("github_topic_moves") or [],
|
||||
github_topic_pad if github_topic_pad else (github_topic or []),
|
||||
wecom_topic,
|
||||
recent_repos=github_recent,
|
||||
)
|
||||
return {
|
||||
"skills_trending": t_items,
|
||||
"skills_hot": h_items,
|
||||
"github_trending": _merge_github_board_items(
|
||||
movement.get("github_trending_moves") or [],
|
||||
github_trending_pad if github_trending_pad else (github_trending or []),
|
||||
wecom_github,
|
||||
recent_repos=recent_board_keys.get("github_trending"),
|
||||
),
|
||||
"github_emerging": _merge_github_board_items(
|
||||
movement.get("github_emerging_moves") or [],
|
||||
github_emerging_pad if github_emerging_pad else (github_emerging or []),
|
||||
wecom_emerging,
|
||||
recent_repos=recent_board_keys.get("github_emerging"),
|
||||
),
|
||||
"github_topic": _merge_github_board_items(
|
||||
movement.get("github_topic_moves") or [],
|
||||
github_topic_pad if github_topic_pad else (github_topic or []),
|
||||
wecom_topic,
|
||||
recent_repos=recent_board_keys.get("github_topic"),
|
||||
),
|
||||
"github_trending": gt_items,
|
||||
"github_emerging": ge_items,
|
||||
"github_topic": gtopic_items,
|
||||
}
|
||||
|
||||
t_flat = [_move_to_skill_row(m) for m in t_moves]
|
||||
@@ -778,11 +882,11 @@ def replace_wecom_board_sections(
|
||||
github_trending: list[dict[str, Any]] | None = None,
|
||||
github_emerging: list[dict[str, Any]] | None = None,
|
||||
github_topic: list[dict[str, Any]] | None = None,
|
||||
wecom_trending: int = 10,
|
||||
wecom_hot: int = 10,
|
||||
wecom_github: int = 10,
|
||||
wecom_emerging: int = 10,
|
||||
wecom_topic: int = 10,
|
||||
wecom_trending: int = 5,
|
||||
wecom_hot: int = 5,
|
||||
wecom_github: int = 5,
|
||||
wecom_emerging: int = 5,
|
||||
wecom_topic: int = 5,
|
||||
pad: bool = False,
|
||||
date_str: str | None = None,
|
||||
trending_pad: list[dict[str, Any]] | None = None,
|
||||
@@ -854,11 +958,11 @@ def replace_wecom_skill_sections(
|
||||
github_trending: list[dict[str, Any]] | None = None,
|
||||
github_emerging: list[dict[str, Any]] | None = None,
|
||||
github_topic: list[dict[str, Any]] | None = None,
|
||||
wecom_trending: int = 10,
|
||||
wecom_hot: int = 10,
|
||||
wecom_github: int = 10,
|
||||
wecom_emerging: int = 10,
|
||||
wecom_topic: int = 10,
|
||||
wecom_trending: int = 5,
|
||||
wecom_hot: int = 5,
|
||||
wecom_github: int = 5,
|
||||
wecom_emerging: int = 5,
|
||||
wecom_topic: int = 5,
|
||||
pad: bool = False,
|
||||
date_str: str | None = None,
|
||||
trending_pad: list[dict[str, Any]] | None = None,
|
||||
|
||||
Reference in New Issue
Block a user