Files
daily-robots/daily/config.py
yumao 36085f107d feat: 新增节假日缓存,仅工作日生成与推送
- daily/holiday.py: 抓取 xiaoai.me 全年节假日并缓存到 .cache/holidays-<year>.json,
  is_workday() 判定(法定节假日/周末休息,调休补班日上班),联网失败回退本地缓存
- scheduler: tick_once 前置工作日闸门,非工作日标记完成并跳过,判定失败降级为正常工作日避免漏跑
- config/.env.example: 新增 DAILY_WORKDAY_ONLY 开关(默认开)
- tests: test_holiday 8 例 + scheduler 非工作日闸门 2 例

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:51:09 +08:00

175 lines
4.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""项目路径与环境变量加载。"""
from __future__ import annotations
import os
from pathlib import Path
from dotenv import load_dotenv
ROOT = Path(__file__).resolve().parent.parent
OUTPUT_DIR = ROOT / "output"
LOG_DIR = ROOT / "logs"
CACHE_DIR = ROOT / ".cache"
SNAPSHOT_FILE = CACHE_DIR / "last-report.json"
# 企微 webhook markdown.content 硬上限
WECOM_MARKDOWN_LIMIT = 4096
def wecom_chunk_bytes() -> int:
"""单条企微 markdown 消息字节上限。"""
chunk = env_int("DAILY_WECOM_CHUNK_BYTES", -1)
if chunk < 0:
chunk = env_int("DAILY_WECOM_MAX_BYTES", WECOM_MARKDOWN_LIMIT)
return min(chunk, WECOM_MARKDOWN_LIMIT)
def wecom_skill_desc_limit() -> int:
"""企微 Skills 榜单条简介建议字数。"""
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)
def news_summary_limit() -> int:
return env_int("DAILY_FULL_NEWS_SUMMARY_LIMIT", 0)
def wecom_max_bytes() -> int:
"""兼容旧配置名。"""
return wecom_chunk_bytes()
load_dotenv(ROOT / ".env")
load_dotenv(ROOT / ".env.local", override=True)
def _clean_env_value(raw: str | None) -> str | None:
if raw is None:
return None
value = raw.strip()
if not value:
return None
# .env 行内注释(未加引号时 python-dotenv 不会自动去掉)
if " #" in value:
value = value.split(" #", 1)[0].rstrip()
return value or None
def env(key: str, default: str | None = None) -> str | None:
return _clean_env_value(os.getenv(key, default))
def env_int(key: str, default: int) -> int:
raw = env(key)
if not raw:
return default
try:
return int(raw)
except ValueError:
return default
def env_bool(key: str, default: bool) -> bool:
raw = env(key)
if raw is None:
return default
return raw.strip().lower() in {"1", "true", "yes", "on"}
def wecom_mode() -> str:
raw = (env("DAILY_WECOM_MODE") or "delta").strip().lower()
return raw if raw in {"full", "delta"} else "delta"
def news_dedup_days() -> int:
return max(1, env_int("DAILY_NEWS_DEDUP_DAYS", 7))
def skip_push_when_silent() -> bool:
return env_bool("DAILY_SKIP_PUSH_WHEN_SILENT", True)
def delta_baseline_fallback() -> str:
raw = (env("DAILY_DELTA_BASELINE_FALLBACK") or "full").strip().lower()
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", 400)
return max(1, env_int("DAILY_BOARD_POOL_SIZE", max(200, 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)
def workday_only() -> bool:
"""仅工作日生成/推送;法定节假日与周末跳过(调休补班日照常)。"""
return env_bool("DAILY_WORKDAY_ONLY", True)