diff --git a/.env.example b/.env.example index d8f1fd9..5b63963 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -# 企微群机器人 webhook(早报推送,与 bot API 模式凭证不同) +# 企微群机器人 webhook(早报推送) WECOM_WEBHOOK_KEY=your-webhook-key # 早报内容 @@ -42,6 +42,21 @@ DAILY_WECOM_CN_AI_NEWS=8 DAILY_WECOM_CHUNK_BYTES=4096 # DAILY_WECOM_MAX_PARTS=5 +# 企微列表模式:delta=仅展示新入榜 | full=全量 Top 榜(回退) +DAILY_WECOM_MODE=delta +# 无历史 data.json 时:full=首日全量一次 | empty=列表为空 +DAILY_DELTA_BASELINE_FALLBACK=full +# 推送闸门不满足时跳过 webhook(仍写 output) +DAILY_SKIP_PUSH_WHEN_SILENT=1 +# DAILY_FORCE_PUSH=1 +# 已推送新闻 link 去重天数 +DAILY_NEWS_DEDUP_DAYS=7 + +# 编辑指定今日首推(可选):关键词,或 关键词|URL +# Python Step 0 检索 → featured.json;Agent / classic 企微「今日首推」优先使用 +# DAILY_FEATURED_PICK=gstack +# DAILY_FEATURED_PICK=gstack|https://github.com/you/gstack + # 国际 AI 时讯(RSS,见 daily/news/feeds.py) DAILY_AI_NEWS=1 # 国内 AI 时讯(RSS,见 daily/news/feeds_cn.py) @@ -69,7 +84,7 @@ DAILY_CN_AI_NEWS=1 # DAILY_DELTA_LOOKBACK_DAYS=7 # DAILY_FULL_DESC_LIMIT=0 # DAILY_FULL_NEWS_SUMMARY_LIMIT=0 -DAILY_AI_NEWS_HOURS=72 +DAILY_AI_NEWS_HOURS=24 DAILY_AI_NEWS_PER_FEED=3 DAILY_AI_NEWS_PER_CATEGORY=5 diff --git a/skills/daily-agent/SKILL.md b/skills/daily-agent/SKILL.md index c61d1e7..8196547 100644 --- a/skills/daily-agent/SKILL.md +++ b/skills/daily-agent/SKILL.md @@ -1,10 +1,11 @@ # 早报 Agent 工作流 -你是 **Skills / GitHub / AI 时讯早报** 的主编 Agent。Python 已完成数据抓取;你分 **两步** 产出可读性强的企微早报。 +你是 **Skills / GitHub / AI 时讯早报** 的主编 Agent。Python 已完成数据抓取;若配置了 `DAILY_FEATURED_PICK`,Python 会先完成 **Step 0 首推检索** 并注入 `featured_pick`。你再分 **两步** 产出可读性强的企微早报。 ## 工作流 ``` +Step 0 首推检索(Python + daily-featured-pick,输出 featured_pick) Step 1 读话题 → 识别热门趋势(输出 trends JSON) Step 2 基于趋势 + 原始数据 → 写企微 Markdown 早报 ``` @@ -27,15 +28,23 @@ Step 2 基于趋势 + 原始数据 → 写企微 Markdown 早报 | `github_emerging` | GitHub **新兴** 当前 Top N | | `github_topic.repos` | GitHub **Topic** 当前 Top N | | `github_topic.topic` | Topic 名称,用于区块标题(如 `llm`) | -| `movement.*_moves` | 较昨日新增(**仅**用于 opening / signals,**不**用于列表区块) | +| `movement.*_moves` | 较昨日新增(**仅**用于 opening / signals;`effective_wecom_mode=delta` 时列表由 Python 插入) | | `movement.*_summary` | 新增摘要(可选写入 signals) | | `ai_news` | 国际 AI 时讯 Top N | | `cn_ai_news` | 国内 AI 时讯 Top N | +| `featured_pick` | **可选**,编辑指定今日首推(Step 0 产出;含 command / why_today / evidence) | **禁止**使用已合并的 `skills_moves` / `github_moves` 自行扩写;**禁止**排名变化、安装涨跌。 Step 1 的 `signals` 与 `top_picks` **优先引用 Top 榜榜首/前列条目、movement 新增与 AI 时讯**。 +### 当输入含 `featured_pick` 时(Step 1) + +- `top_picks.skill` **必须**使用 `featured_pick`:`id`/`title`/`command`/`url` 来自 featured,`why` 来自 `featured_pick.why_today` +- `opening` 首句优先引用 `featured_pick.evidence` 中的数字或条目名 +- `signals` 至少 1 条与 featured 相关 +- featured 不在 Top 榜时仍可作为首推,`why` 须来自 `featured_pick.evidence`,禁止编造榜单排名 + **只输出 JSON:** ```json @@ -50,7 +59,7 @@ Step 1 的 `signals` 与 `top_picks` **优先引用 Top 榜榜首/前列条目 } ], "top_picks": { - "skill": { "id": "owner/repo/skill", "title": "...", "why": "中文,为什么今天首推" }, + "skill": { "id": "owner/repo/skill", "title": "...", "command": "npx skills add ...", "why": "中文,为什么今天首推" }, "github": { "repo": "owner/repo", "why": "中文" }, "news": { "link": "完整URL", "title_zh": "中文标题", "why": "中文一句话" } }, @@ -67,13 +76,13 @@ Step 1 的 `signals` 与 `top_picks` **优先引用 Top 榜榜首/前列条目 - `opening` 遵循 **article-writing Newsletter** 规则:首句用数字/条目名/新闻标题开头,不用「今天有三条线」「值得关注」等空框架 - `signals` 3–5 条,每条单行,可含 emoji 前缀;与 `opening` 不重复同一句信息 - `top_picks.why` 用「事实/数字 + 一句判断」,不用空泛形容词 -- `top_picks` 必须引用输入中真实存在的 id/repo/link +- `top_picks` 必须引用输入中真实存在的 id/repo/link;有 `featured_pick` 时 skill 首推以 featured 为准 --- ## Step 2:撰写企微早报 -你会收到 **原始数据 JSON** + **Step 1 的 trends JSON**。 +你会收到 **原始数据 JSON**(含可选 `featured_pick`)+ **Step 1 的 trends JSON**。 **只输出企微 Markdown 正文**(不要代码块包裹,不要解释)。 @@ -93,8 +102,16 @@ Step 1 的 `signals` 与 `top_picks` **优先引用 Top 榜榜首/前列条目 > {signal 3} 📦 **今日首推** -`npx skills add {source}/{skill}` -> {top_picks.skill.why} +[{featured_pick.title 或 repo 或 skill 名}]({featured_pick.url 或 command 或 skills.sh 链接}) +> {featured_pick.why_today 或 top_picks.skill.why} + +### 当输入含 `featured_pick` 时(Step 2) + +- **今日首推**链接行用 Markdown `[标题](URL)`,与新闻条目同格式;**不要**用反引号裸 URL 或裸 `npx` 命令 +- GitHub 首推:标题用 `owner/repo`(如 `[garrytan/gstack](https://github.com/...)`) +- Skill 首推:标题用 skill 名,链接用 `featured_pick.url` 或 `https://skills.sh/{id}` +- 说明用 `featured_pick.why_today`;**不要**改回 Trending 榜首 +- 读者可见文案(opening / 今日首推说明 / why)**不得**写「编辑指定首推」等元信息,只陈述项目事实与判断 🌍 **国际 AI · 精选 10** 1. [{title_zh}]({link}) — {why 或摘要} @@ -104,10 +121,10 @@ Step 1 的 `signals` 与 `top_picks` **优先引用 Top 榜榜首/前列条目 1. [{title}]({link}) — {why 或摘要} 2. ...(**必须 8 条**,来自 `cn_ai_news`,按重要性排序;标题已是中文,可微调润色) - + 🐙 **GitHub Trending Top {N}** - + 1. [{repo}]({url}) · {lang} · ⭐{stars} — {中文一句话} 🌱 **GitHub 新兴 Top {N}** @@ -121,14 +138,15 @@ Step 1 的 `signals` 与 `top_picks` **优先引用 Top 榜榜首/前列条目 ### 榜单选取规则(top_n) -1. **五个 GitHub 区块分开写**:GitHub Trending / 新兴 / Topic,**禁止合并** +1. **五个 GitHub 区块分开写**(仅 `effective_wecom_mode=full`):GitHub Trending / 新兴 / Topic,**禁止合并** 2. **Skills Trending / Hot 由 Python 自动插入**,Agent 不要写这两段 -3. **禁止**改用 `movement.*_moves` 作为列表来源;movement 仅用于 opening / signals 描述「今日新增」 +3. 当 `data.effective_wecom_mode` 为 `delta` 时:**禁止写** Skills Trending / Hot / GitHub 列表区块(Python 插入变化列表);movement 可用于 opening / signals。`full` 模式保持原 GitHub 列表规则 4. **禁止**在条目后写 `(新入 … #n)` 类括号标注 -5. **即使某榜较昨日无新增,仍须完整列出 Top 榜条目** +5. `effective_wecom_mode=full` 时:即使某榜较昨日无新增,仍须完整列出 Top 榜条目 6. **国际 AI 必须 10 条**(来自 `ai_news`) 7. **国内 AI 必须 8 条**(来自 `cn_ai_news`;无数据时写「暂无可用条目」) 8. 禁止排名变化、安装涨跌、连霸描述 +9. 榜全稳(movement 各 `*_stable` 为 true)时,signals 聚焦新闻与首推,不编造榜单变化 ```markdown 📈 **Skills Trending Top 10** @@ -161,6 +179,7 @@ Step 1 的 `signals` 与 `top_picks` **优先引用 Top 榜榜首/前列条目 **禁止(Banned Patterns):** - 「据悉」「值得关注」「快速演进」「In today's rapidly evolving landscape」 +- 「编辑指定首推」「编辑今日首推」等内部流程用语(读者不应感知编辑配置) - 「今天有三条线叠在一起」这类无证据的空框架开场 - 无证据的「为什么这很重要」「 here's why this matters」 - 结尾硬塞互动问句(如「你怎么看?」「值得花十分钟扫一眼」) diff --git a/tests/test_wecom_delta.py b/tests/test_wecom_delta.py index 9e72085..fb7dbb3 100644 --- a/tests/test_wecom_delta.py +++ b/tests/test_wecom_delta.py @@ -188,3 +188,61 @@ class PushGateTests(unittest.TestCase): ) self.assertTrue(gate.should_push) self.assertIn("force_push", gate.reasons) + + +class WebhookSilentTests(unittest.TestCase): + def test_skip_when_silent(self): + import json + import tempfile + from pathlib import Path + + from daily.webhook import should_skip_push + + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + data = root / "2026-07-10.data.json" + data.write_text( + json.dumps( + {"meta": {"push_gate": {"should_push": False, "silent": True, "reasons": []}}} + ), + encoding="utf-8", + ) + report = root / "2026-07-10.wecom.md" + report.write_text("📰 test", encoding="utf-8") + self.assertTrue(should_skip_push(report)) + + +class E2ESmokeTests(unittest.TestCase): + def test_delta_mode_no_full_top_label(self): + from daily.format_wecom import replace_wecom_board_sections + + md = "📰 **早报 · 2026-07-10**\n\n🌍 **国际 AI · 精选 1**\n1. [x](https://a.com)\n" + movement = { + "skills_trending_moves": [ + { + "id": "a/b/c", + "rank": 2, + "title": "c", + "source": "a/b", + "installs": 1, + "link": "https://skills.sh/a/b/c", + "description": "d", + } + ], + "skills_hot_moves": [], + "github_trending_moves": [], + "github_emerging_moves": [], + "github_topic_moves": [], + } + with patch("daily.format_wecom.localize_brief_descriptions", return_value={}): + with patch("daily.format_wecom.needs_chinese", return_value=False): + out = replace_wecom_board_sections( + md, + mode="delta", + movement=movement, + trending=[], + hot=[], + topic_name="llm", + ) + self.assertIn("Skills Trending 变化", out) + self.assertNotIn("Skills Trending Top", out)