feat: 拆分 wecom_shown_keys 与 movement_baseline 历史层
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
83
daily/board_history.py
Normal file
83
daily/board_history.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user