- 新增常驻调度器 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>
14 KiB
Design: 企微早报多样性与去重
Generated: 2026-07-14
Repo: daily-robots
Status: APPROVED
Mode: Builder
Related: docs/design-wecom-delta-mode.md(Delta 列表模式)
Revision: office-hours A —— 拆分展示历史、单一列表主人、历史真相表(2026-07-14)
Problem Statement
近两日企微早报(如 2026-07-13 / 07-14)骨架相同,且:
- 今日首推连续两天同为
headroom - 开场主题同属「上下文压缩 + 视频/Skills」腔调
- AI 时讯出现「放宽窗口」旧闻凑数标注
- Skills / GitHub 各榜(尤其新兴榜)周内大量重复展示
读者需要「今天新信息」,而不是换日期的复印机。
Decisions(已确认)
| 决策点 | 选择 |
|---|---|
| 实现路径 | A:管线选择器(代码硬保证去重;LLM 只写开场/理由/摘要;中文化仍走现有 localize) |
| Skills「同一类」 | 暂不管;沿用现有 group_skills_by_source(已知残留:同 source 换 skill id 仍可能周内再出现) |
| 周去重后不足 Top N | 深池补满,仍保证周内未出现;池空则短榜,不破周约束 |
| 首推改推候选 | 先展示榜(池 A),再 raw 深池(池 B);一月内首推不重复 |
| 开场主题 | 近 7 天禁主题/句式(软)+ 叙事轴与近 3 天不同(硬,代码选轴) |
| 取消「放宽窗口」 | 禁止旧闻/已推凑数;不够则深度检索补新闻;禁止任何「放宽」文案标注;仍不够则短列表 |
| 展示历史 vs 异动基准 | 必须拆开:movement_baseline ≠ wecom_shown_keys |
| full/delta 列表主人 | 唯一主人 = board_select(含 full 与 delta 的 moves∪pad);禁止二次独立选榜 |
Explicit Non-Goals
| 项 | 状态 |
|---|---|
| 语义级 Skill「同一类」分类 | ❌ 本期不做 |
| 同日 Skills Trending ↔ Hot 互斥 | ❌ 本期不做 |
| 独立 editorial 微服务 | ❌ 不做 |
改写 localize 为脚本机翻 |
❌ 保持 LLM + 缓存 |
编辑指定首推豁免改推(FEATURED_FORCE) |
❌ 本期不做 |
| 「开场关键短语」硬匹配算法 | ❌ 本期不做(仅 prompt 软约束;不进硬成功标准) |
Recommended Approach: 管线选择器(路径 A)
在现有 daily generate 内增加选条/裁决层,不新起进程:
采集 raw 榜 + 新闻
→ build movement_baseline(raw Top compare_depth —— 仅供次日「新入榜」,禁止写展示历史)
→ board_select(读 wecom_shown_keys 周历史;周去重 + 深池;输出当日最终展示列表)
· full:直接取 board_select 结果前 N
· delta:在 board_select 候选池内做 moves∪pad(pad 也只从该池/同规则深池取,不再另起一套历史)
→ featured_resolve(与昨日首推相同则改推;池 A = 本 run 最终展示 keys)
→ research why(先定人,再写 why_today)
→ news_select(禁放宽凑数;深检索补满;剥「放宽」前缀)
→ editorial(代码选 narrative_axis;prompt 附近 7 天 theme 软禁)
→ 渲染 wecom
→ 写回 wecom_shown_keys = 最终进入企微正文的榜条目 keys(post-render)
→ 其余 history(首推月、axis)写入 data.json 约定字段
LLM 负责:opening / theme_line、首推 why_today、新闻与榜单项中文摘要(localize)。
代码负责:谁上榜、首推换谁、周/月去重、narrative_axis 选取、是否允许旧闻。
Approaches Considered
| A 管线选择器(采用) | B 偏 LLM 约束 | C 独立 editorial 服务 | |
|---|---|---|---|
| 优点 | 可测;与 pushed-links 模式一致 | 改 prompt 快 | 边界清晰 |
| 缺点 | 需动 generate / featured / news / format | 易漏、难测 | 过重 |
Section 1 — 总览、列表主人、历史真相表
1.1 两种「基准」禁止混用
| 字段 | 含义 | 写入时机 | 读者 |
|---|---|---|---|
movement_baseline |
Raw 各榜 Top compare_depth(现网语义不变) |
build_llm_input / 采集后尽早 |
build_movement_context(新入榜) |
wecom_shown_keys |
读者实际见到的各榜 key 集合(及可选 rank) | wecom 渲染完成之后 | board_select 周去重;delta pad;测试 |
禁止:把 wecom_shown_keys 写入或覆写 movement_baseline。
禁止:让 load_recent_board_keys 继续读 movement_baseline 充当「已展示」——应改为读近 N 日 wecom_shown_keys(可保留函数名,换数据源;或新建 load_recent_shown_keys)。
1.2 唯一列表主人
board_select(模块可挂在 daily/board_select.py 或扩 delta.py)是各榜最终展示行的唯一生产者:
| 模式 | 行为 |
|---|---|
full |
board_select(raw, shown_history) → 至多 N 条,直接渲染 |
delta |
先算相对 movement_baseline 的 moves;展示 = moves(已在候选内)∪ pad;pad 候选必须来自同一周去重池(与 full 同一套 board_select 规则),不得再读 raw baseline 当「已展示」 |
交互影响(非「完全正交」):周去重会减少可展示重复项 → delta 日可能更短、silent/gate 行为可能变化。DAILY_WECOM_MODE 枚举语义不变,但列表密度会变。
1.3 历史真相表(单一来源)
全部落在 output/{date}.data.json(新闻 pushed-links 例外,沿用现网 cache)。
| 字段路径 | 窗口 | Key 规则 | 写者 | 读者 |
|---|---|---|---|---|
data.movement_baseline |
次日对比用 | raw 条目切片 | build_movement_baseline |
movement |
data.wecom_shown_keys.{board} |
滚动 7 天(读近 7 日文件) | Skills:与现网 _skill_keys_in_board_item / skill_id 一致;GitHub:owner/repo |
post-render persist | board_select / pad |
data.featured_pick_key |
滚动 30 天 | skill id 或 owner/repo |
featured_resolve 成功后 |
月去重 |
data.narrative_axis |
滚动 3 天 | 枚举字符串 | 代码 pick_narrative_axis |
Step 1 约束 / 校验 |
data.theme_line / trends opening |
近 7 日供 prompt | 原文 | editorial 落盘 | Step 1 软禁(不硬匹配) |
CACHE_DIR/pushed-news-links.json |
DAILY_NEWS_DEDUP_DAYS |
规范化 URL | 推送成功后 | news filter |
不另建平行 CACHE「board-history.json」,避免双源漂移。冷启动:缺文件 = 空集合。
1.4 数据流挂点
| 逻辑 | 挂点 |
|---|---|
movement_baseline |
现网:raw 榜入库时(不变) |
board_select |
渲染前;输出写入供 Agent/llm_input 与 wecom 共用的最终列表字段 |
| delta pad | 调用同一周去重历史(wecom_shown_keys),不再独立解释 movement_baseline 为展示史 |
featured_resolve |
先于 why 检索;池 A = 本 run board_select(delta 则为本 run 最终展示列表) |
| 新闻 | 所有 prepare 路径关 backfill;research SKILL 改文案规则;后处理剥「放宽」 |
narrative_axis |
代码先选轴再注入 Step 1;LLM 不得另选冲突轴 |
wecom_shown_keys 写回 |
replace_wecom_* / build_wecom_report 之后,与最终正文列表一致 |
Section 2 — 各榜选条 + 今日首推改推
2.1 board_select(五榜共用)
适用:skills_trending / skills_hot / github_trending / github_emerging / github_topic。
输入:当日 raw 池(深,pool ≥ DAILY_BOARD_POOL_SIZE)
历史:近 DAILY_BOARD_DEDUP_DAYS 的 wecom_shown_keys[board]
输出:至多 N 条(N = 现有 wecom Top 配置)
1. 现有整理(Skills:source 合并;GitHub:repo key)
2. 滤掉近 7 天该榜 wecom_shown_keys
3. 按原排名取前 N
4. 不足 → 继续扫深池,仍排除周历史,直到满 N 或池空
5. 池空仍不足 → 短榜;日志 board_short:{board}:{n};不回填周内已展示条目
分榜独立历史:Trending 出过的 skill,Hot 仍可出。
Post-render:将实际写入企微的 keys 写入当日 wecom_shown_keys(测试断言:history ⊆ / == 渲染列表,≠ movement_baseline)。
2.2 featured_resolve
触发:本 run 拟用首推身份与前一天 featured_pick_key(或等价 data 字段)相同。
身份函数:skill → skill_id;github → owner/repo。
含 DAILY_FEATURED_PICK 与自动首推;本期不豁免。无昨日文件 → 不改推。
顺序(硬):定候选 → 再 research/why_today(禁止先写旧条目 why 再改人却不重写)。
候选:
- 池 A:本 run 最终会展示的 Skills + GitHub 榜条目(与
wecom_shown_keys同源结构) - 池 B:raw 深池中尚未进入本 run 展示者
过滤:近 30 天 featured_pick_key;排除冲突项自身。
抽取:hash(date_str + "featured") 可复现;测试可注入 RNG。先 A 后 B;仍空 → 保留原首推 + featured_fallback_exhausted。
落盘:data.featured_pick_key。
Section 3 — 开场主题 + AI 时讯
3.1 开场主题
| 机制 | 强度 | 规则 |
|---|---|---|
narrative_axis |
硬 | 代码 pick_narrative_axis(used_last_N) 从剩余枚举选取;注入 prompt;LLM 输出须等于该轴;冲突则重试 1 次,再失败则强制覆写为代码所选轴再落盘(保证成功标准可测) |
| theme/opening 软禁 | 软 | prompt 附近 7 天 theme_line/opening 摘要;禁止复述;无 n-gram 硬匹配;不列入硬成功标准 |
叙事轴枚举:
政策监管 · 模型发布 · 工具链/Agent · 芯片算力 · 开源生态 · 应用落地 · 安全/诉讼
opening 首句证据须来自当日数据;首推改推后须跟新首推或当日主轴新闻。
3.2 AI 时讯:取消「放宽窗口」
目标条数 = 现网配置之和(如 DAILY_WECOM_AI_NEWS + tech/CN 等),文档不写死「15」。
- 所有 prepare 路径关闭「不够塞回已推/旧条」(
DAILY_NEWS_BACKFILL=0默认);pushed-news-links过滤保留。 - 不够 → 深度检索补新闻(须:https link、未 pushed、可核实);补入年龄上限 =
DAILY_AI_NEWS_HOURS(与主窗一致),禁止借 research 变相放宽到任意旧闻。 - 改 research SKILL:删除「放宽至 48h 并注明」;后处理剥
放宽窗口/放宽至前缀或丢弃。 - 仍不足 → 短列表 +
news_short:{n}。
中文化:daily/localize.py(不变)。
Section 4 — 配置、错误处理、测试
4.1 环境变量
| 变量 | 默认 | 含义 |
|---|---|---|
DAILY_BOARD_DEDUP_DAYS |
7 |
读 wecom_shown_keys 的滚动天数 |
DAILY_BOARD_POOL_SIZE |
≥50 / 与现有 skill pool 对齐 | 深池扫描深度 |
DAILY_FEATURED_DEDUP_DAYS |
30 |
今日首推月去重 |
DAILY_THEME_BAN_DAYS |
7 |
软禁:注入 prompt 的 theme 天数 |
DAILY_NARRATIVE_AXIS_DAYS |
3 |
叙事轴互斥窗 |
DAILY_NEWS_BACKFILL |
0 |
0=禁止旧闻凑数 |
DAILY_NEWS_DEDUP_DAYS |
已有 7 |
pushed-links |
DAILY_DELTA_PAD_LOOKBACK_DAYS 应与 DAILY_BOARD_DEDUP_DAYS 对齐,且 pad 与 board_select 共用 wecom_shown_keys(窗口对齐不够,数据源必须同一)。
4.2 错误与降级
| 情况 | 行为 |
|---|---|
无 wecom_shown_keys 历史 |
空集合,正常满榜 |
| 周去重后深池不足 | 短榜 + board_short |
| 首推冲突且 A/B 空 | 保留原首推 + featured_fallback_exhausted |
| LLM 轴与代码轴冲突 | 覆写为代码轴 |
| 深检索仍不足时讯 | 短列表;禁止 backfill |
| history 读写失败 | 当次按空历史 + error 日志 |
4.3 测试(pytest)
board_select:假wecom_shown_keys+ 深池 → 无周交集;深池补满;不足短榜- 回归钉死:写回后
wecom_shown_keys≠ 用movement_baseline推导的集合(构造 raw Top 与展示 Top 故意不同) - delta:pad 不引入近 7 日
wecom_shown_keys内 key featured_resolve:先定人再 why;A 优先 B;月未见;可注入 RNG- news:
BACKFILL=0;剥「放宽*」;research 补入不超 hours 窗 pick_narrative_axis:近 3 天互斥;落盘轴 == 代码轴- 既有 delta / pushed_links / wecom 回归不挂
4.4 成功标准(硬)
- 连续两天:首推 key 不同(除非
featured_fallback_exhausted) - 同一榜近 7 日
wecom_shown_keys:无重复 key(池足够时) - 时讯:无「放宽*」标注;无 backfill 已推 link
- 近 3 天
narrative_axis:两两不同(代码保证)
软标准(不闸门):opening 读感不像连续复印。
Implementation Sketch(非计划明细)
data.json增加wecom_shown_keys;改load_recent_*数据源board_select+ 让 delta pad 共用- post-render persist shown keys
featured_resolve时序修正- news backfill off + SKILL + 剥前缀
pick_narrative_axis+ prompt 注入- 测试如上
正式任务拆解 → writing-plans。
Office-hours Review Notes
- 对抗审阅质量约 4/10 → 本修订处理三大硬伤(存储拆分、列表主人、真相表)。
- 未纳入本期(原选项 B):首推质量加权、关键短语硬匹配。
- 已知残留:source 级「同类」周内可再现。
Spec Self-Review
movement_baseline与wecom_shown_keys职责分离写死- 单一列表主人 + full/delta 交互说明
- 历史真相表无「与/或」双源
- 轴硬 / 短语软;成功标准不含无法验证的短语匹配
- 周不足=深池、首推=A→B、新闻禁放宽 与访谈一致