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:
2026-07-17 18:12:00 +08:00
parent 6192dd4e2a
commit 6ea2a4e4c6
35 changed files with 4389 additions and 359 deletions

View File

@@ -0,0 +1,156 @@
"""Tests for news time window filtering."""
from __future__ import annotations
import os
import unittest
from datetime import datetime, timezone, timedelta
from unittest.mock import patch
from zoneinfo import ZoneInfo
from daily.news.fetch import _cutoff_datetime, _parse_datetime, _within_window
class NewsWindowTests(unittest.TestCase):
def test_cutoff_floor_today_excludes_yesterday_even_within_24h(self):
tz = ZoneInfo("Asia/Shanghai")
# 2026-07-09 09:00 CST = 2026-07-09 01:00 UTC
fixed = datetime(2026, 7, 9, 1, 0, tzinfo=timezone.utc)
with patch("daily.news.fetch._now_utc", return_value=fixed):
with patch.dict(os.environ, {"DAILY_AI_NEWS_HOURS": "24"}, clear=False):
cutoff = _cutoff_datetime(floor_today=True)
start_today_cst = datetime(2026, 7, 9, 0, 0, tzinfo=tz).astimezone(timezone.utc)
self.assertEqual(cutoff, start_today_cst)
yesterday = datetime(2026, 7, 8, 20, 0, tzinfo=tz).astimezone(timezone.utc)
self.assertFalse(_within_window({"published": yesterday.isoformat()}, cutoff))
def test_cutoff_rolling_only_includes_last_24h(self):
fixed = datetime(2026, 7, 9, 12, 0, tzinfo=timezone.utc)
with patch("daily.news.fetch._now_utc", return_value=fixed):
with patch.dict(os.environ, {"DAILY_AI_NEWS_HOURS": "24"}, clear=False):
cutoff = _cutoff_datetime(floor_today=False)
self.assertEqual(cutoff, fixed - timedelta(hours=24))
def test_within_window_rejects_missing_datetime(self):
cutoff = datetime(2026, 7, 9, 0, 0, tzinfo=timezone.utc)
self.assertFalse(_within_window({"title": "x", "link": "https://a.com"}, cutoff))
def test_parse_date_only_uses_local_noon(self):
with patch.dict(os.environ, {"DAILY_AI_NEWS_TZ": "Asia/Shanghai"}, clear=False):
dt = _parse_datetime("2026-07-09")
self.assertIsNotNone(dt)
assert dt is not None
local = dt.astimezone(ZoneInfo("Asia/Shanghai"))
self.assertEqual(local.hour, 12)
class NewsFormatTests(unittest.TestCase):
def test_ai_news_lines_use_desc_as_link_text(self):
from daily.format_wecom import _ai_news_lines
lines = _ai_news_lines(
[
{
"title": "English Title",
"link": "https://example.com/a",
"source_name": "Src",
"published_fmt": "07-11",
"desc_short": "中文摘要一句",
}
]
)
self.assertEqual(len(lines), 1)
self.assertIn("[中文摘要一句](https://example.com/a)", lines[0])
self.assertNotIn("English Title", lines[0])
self.assertNotIn("> ", lines[0])
def test_ai_news_lines_fallback_to_title(self):
from daily.format_wecom import _ai_news_lines
lines = _ai_news_lines(
[
{
"title": "仅标题",
"link": "https://example.com/b",
"source_name": "Src",
"published_fmt": "",
"desc_short": "",
}
]
)
self.assertIn("[仅标题](https://example.com/b)", lines[0])
class NewsSummaryTests(unittest.TestCase):
def test_brief_news_summary_no_ellipsis(self):
from daily.news.fetch import brief_news_summary
text = (
"Meta told Dylan Byers, of Puck News, that the company removed "
"the controversial AI feature after user backlash on Instagram."
)
out = brief_news_summary(text, limit=72)
self.assertNotIn("...", out)
self.assertLessEqual(len(out), 72)
self.assertTrue(out.startswith("Meta told"))
def test_brief_news_summary_filters_junk(self):
from daily.news.fetch import brief_news_summary
self.assertEqual(brief_news_summary("点击查看原文>"), "")
self.assertEqual(brief_news_summary("Article URL: https://example.com"), "")
def test_sync_wecom_news_rows_after_localize(self):
from daily.news.fetch import _to_wecom_news_row, sync_wecom_news_rows
row = _to_wecom_news_row(
{
"title": "t",
"link": "https://a.com/x",
"source_name": "s",
"published_fmt": "07-11",
"summary": "Short english stub that was truncated early...",
}
)
flat = [
{
"link": "https://a.com/x",
"summary": "苹果指控 OpenAI 窃取硬件商业机密,诉讼称 misconduct 涉及多名前员工。",
}
]
sync_wecom_news_rows([row], flat)
self.assertNotIn("...", row["desc_short"])
self.assertIn("苹果", row["desc_short"])
def test_finalize_wecom_news_forces_chinese(self):
from daily.news.fetch import finalize_wecom_news_items
items = [
{
"link": "https://a.com/1",
"desc_short": "Meta removed the feature after backlash.",
"summary_plain": "Meta removed the feature after backlash.",
}
]
with patch(
"daily.localize.localize_brief_descriptions",
return_value={"wecom-news:https://a.com/1": "Meta 在舆论压力下移除了该功能"},
):
finalize_wecom_news_items(items, force_chinese=True)
self.assertIn("Meta", items[0]["desc_short"])
self.assertNotIn("backlash", items[0]["desc_short"])
class NewsPickTests(unittest.TestCase):
def test_pick_and_backfill_to_limit(self):
from daily.news.fetch import _fill_picked_to_limit, _pick_news_items
flat = [
{"link": f"https://a.com/{i}", "title": f"t{i}", "category_id": "media", "summary": "s"}
for i in range(12)
]
picked = _pick_news_items(flat, 10, ("media",))
self.assertEqual(len(picked), 10)
picked = _fill_picked_to_limit(picked[:3], [flat], 10)
self.assertEqual(len(picked), 10)