feat: 拆分 wecom_shown_keys 与 movement_baseline 历史层

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 10:31:49 +08:00
parent 02e97e057f
commit f563239e0b
6 changed files with 551 additions and 5 deletions

83
daily/board_history.py Normal file
View File

@@ -0,0 +1,83 @@
"""企微展示历史:读写 data.wecom_shown_keys与 movement_baseline 严格分离。"""
from __future__ import annotations
import json
import logging
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any
from daily.config import OUTPUT_DIR, board_dedup_days
from daily.delta import RECENT_BOARD_KEYS, skill_id
logger = logging.getLogger(__name__)
BOARD_KEYS = RECENT_BOARD_KEYS
def extract_shown_keys(board: str, items: list[dict[str, Any]]) -> list[str]:
"""从最终展示 items 抽取稳定 identity key。"""
keys: list[str] = []
seen: set[str] = set()
for item in items:
if board.startswith("skills_"):
key = skill_id(item)
else:
key = str(item.get("repo") or "")
if not key or key in seen:
continue
seen.add(key)
keys.append(key)
return keys
def load_recent_shown_keys(
date_str: str,
*,
lookback_days: int | None = None,
) -> dict[str, set[str]]:
"""近 N 日 data.wecom_shown_keys 并集(不含当日)。缺省或读失败视为空集。"""
empty = {board: set() for board in BOARD_KEYS}
try:
dt = datetime.strptime(date_str, "%Y-%m-%d")
except ValueError:
return empty
days = lookback_days if lookback_days is not None else board_dedup_days()
out: dict[str, set[str]] = {board: set() for board in BOARD_KEYS}
for day_offset in range(1, days + 1):
prev_date = (dt - timedelta(days=day_offset)).strftime("%Y-%m-%d")
path = OUTPUT_DIR / f"{prev_date}.data.json"
if not path.exists():
continue
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError) as exc:
logger.warning("读取 wecom_shown_keys %s 失败:%s", path, exc)
continue
data = payload.get("data")
if not isinstance(data, dict):
continue
shown = data.get("wecom_shown_keys")
if not isinstance(shown, dict):
continue
for board in BOARD_KEYS:
keys = shown.get(board) or []
if not isinstance(keys, list):
continue
out[board].update(str(k) for k in keys if k)
return out
def merge_wecom_shown_into_data(
data: dict[str, Any],
shown: dict[str, list[str]],
) -> dict[str, Any]:
"""写入 wecom_shown_keys不修改 movement_baseline。"""
merged = dict(data)
merged["wecom_shown_keys"] = {
board: list(keys) for board, keys in shown.items()
}
return merged

View File

@@ -30,6 +30,24 @@ def wecom_skill_desc_limit() -> int:
return env_int("DAILY_WECOM_SKILL_DESC_LIMIT", 56)
def wecom_news_desc_limit() -> int:
"""企微新闻摘要建议字数;在句读/词边界截断,不加省略号。"""
return max(24, env_int("DAILY_WECOM_NEWS_DESC_LIMIT", 72))
def wecom_ai_news_tech_limit() -> int:
"""research 模式下技术类时讯条数(叠加在 DAILY_WECOM_AI_NEWS 之上)。"""
return max(0, env_int("DAILY_WECOM_AI_NEWS_TECH", 5))
def wecom_pad_pool_size(display_limit: int) -> int:
"""Delta 补榜候选池大小(展示条数之上多取,避免去重后凑不满)。"""
explicit = env_int("DAILY_WECOM_PAD_POOL", -1)
if explicit > 0:
return explicit
return max(display_limit * 5, 50)
def full_desc_limit() -> int:
"""完整版早报摘要长度0 表示不截断。"""
return env_int("DAILY_FULL_DESC_LIMIT", 0)
@@ -99,5 +117,53 @@ def delta_baseline_fallback() -> str:
return raw if raw in {"full", "empty"} else "full"
def wecom_delta_pad() -> bool:
"""Delta 模式下新入榜优先,不足时用当日 Top 榜补满;补榜排除近 N 天 baseline 已出现条目。"""
return env_bool("DAILY_WECOM_DELTA_PAD", True)
def delta_pad_lookback_days() -> int:
"""补榜时排除近 N 天 baseline 已出现过的条目(默认与异动对比窗口一致)。"""
fallback = env_int("DAILY_DELTA_LOOKBACK_DAYS", 7)
return max(1, env_int("DAILY_DELTA_PAD_LOOKBACK_DAYS", fallback))
def force_push() -> bool:
return env_bool("DAILY_FORCE_PUSH", False)
def schedule_timezone_name() -> str:
return (env("DAILY_SCHEDULE_TZ") or "Asia/Shanghai").strip() or "Asia/Shanghai"
def schedule_generate_at() -> str:
return (env("DAILY_SCHEDULE_GENERATE_AT") or "08:50").strip() or "08:50"
def schedule_push_at() -> str:
return (env("DAILY_SCHEDULE_PUSH_AT") or "09:00").strip() or "09:00"
def board_dedup_days() -> int:
return max(1, env_int("DAILY_BOARD_DEDUP_DAYS", 7))
def board_pool_size() -> int:
fallback = env_int("DAILY_WECOM_SKILL_POOL", 50)
return max(1, env_int("DAILY_BOARD_POOL_SIZE", max(50, fallback)))
def featured_dedup_days() -> int:
return max(1, env_int("DAILY_FEATURED_DEDUP_DAYS", 30))
def theme_ban_days() -> int:
return max(1, env_int("DAILY_THEME_BAN_DAYS", 7))
def narrative_axis_days() -> int:
return max(1, env_int("DAILY_NARRATIVE_AXIS_DAYS", 3))
def news_backfill_enabled() -> bool:
return env_bool("DAILY_NEWS_BACKFILL", False)

View File

@@ -15,6 +15,15 @@ logger = logging.getLogger(__name__)
KeyFn = Callable[[dict[str, Any]], str]
RECENT_BOARD_KEYS = (
"skills_trending",
"skills_hot",
"github_trending",
"github_emerging",
"github_topic",
)
def compare_depth() -> int:
return env_int("DAILY_DELTA_COMPARE_DEPTH", 15)
@@ -39,6 +48,23 @@ def _key_set(items: list[dict[str, Any]], key_fn: KeyFn, *, depth: int) -> set[s
return {key_fn(item) for item in items[:depth] if key_fn(item)}
def load_recent_board_keys(
date_str: str,
*,
lookback_days: int | None = None,
) -> dict[str, set[str]]:
"""近 N 天各榜已展示过的 skill id / repo不含当日供补榜去重
委托 board_history.load_recent_shown_keys只读 wecom_shown_keys
不读 movement_baseline。
"""
from daily.board_history import load_recent_shown_keys
from daily.config import board_dedup_days
days = lookback_days if lookback_days is not None else board_dedup_days()
return load_recent_shown_keys(date_str, lookback_days=days)
def find_previous_data(date_str: str) -> tuple[str, dict[str, Any]] | None:
"""查找最近一份早于 date_str 的 data.json。"""
try: