feat(news): add news002 morning-report template with dedup gate and card images
Freeze the 2026-09-18 layout as news001 and add news002 alongside it. Analysis of the 0918 production page found 49 news cards covering only ~20 distinct events (4x "王毅同鲁比奥通电话", 5x "东博会南宁开幕"), citing just 7 URLs that are all section landing pages rather than article permalinks, with zero images. The root cause is structural: 23 fixed sections with fixed card quotas force padding when the day yields fewer events. news002 changes the contract so events decide sections, not the reverse: core sections always render, flex sections are dropped entirely when they have no unused event, total story cards are capped at 28, and every card carries a data-event-key fingerprint. Add wechat-news-morning-dedup.mjs as a post-generation gate, since the prompt level "no duplicates" rule is unverifiable. It dedupes by event key, then by article permalink (deliberately ignoring section landing pages, which would otherwise delete most of the page), then by CJK bigram title similarity, since the existing whitespace-tokenised matcher does not work on Chinese titles. Carry per-item images through normalizeSearchResult into the prefetch brief so cards can render real article thumbnails with a placeholder fallback. Teach the WeChat inline converter both DOM variants (lead-card, masthead meta, lede). Per-card images in the WeChat draft body remain a known gap. Template selection is config/env driven and still defaults to news001. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
# 每日新闻早报模板:news001 / news002
|
||||
|
||||
模板定义集中在 [`news-morning-templates.mjs`](../news-morning-templates.mjs)。
|
||||
早报页由 Agent 按 spec 生成 HTML,仓库里没有静态页模板文件,所以「模板」= 交给 Agent 的版式与内容规范。
|
||||
|
||||
| 模板 | 版本 | 状态 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `news001` | `2026-09-18-refined` | frozen | 2026-09-18 定稿版式,已上生产。**只读,不允许就地修改**;要改版式请新开 `news003`。 |
|
||||
| `news002` | `2026-09-21-subscriber` | active | 订阅向改版:事件唯一化、每条配图、报头版式、深色模式。 |
|
||||
|
||||
## 切换模板
|
||||
|
||||
```bash
|
||||
# 环境变量(默认 news001)
|
||||
H5_WECHAT_NEWS_MORNING_TEMPLATE_ID=news002
|
||||
```
|
||||
|
||||
也可以写进 DB 配置 `h5_wechat_admin_config.news_morning_draft` 的 `templateId` 字段,
|
||||
DB 值优先于环境变量。非法值一律回落到 `news001`。
|
||||
|
||||
## news001 的三个实测问题
|
||||
|
||||
以生产页 `daily-news-0918.html` 为样本(`node scripts/preview-news002-template.mjs` 旁的分析脚本可复现):
|
||||
|
||||
1. **重复选题**。49 张新闻卡片里只有约 20 个独立事件。「王毅同鲁比奥通电话」出现 4 次,
|
||||
「东博会南宁开幕」5 次,「OpenAI 霍奇猜想」5 次。
|
||||
根因是版式把「23 个栏目 × 固定条数」当硬指标,当天独立事件不够时 Agent 就换个标题再写一遍。
|
||||
2. **来源不可核验**。49 张卡片只引用了 **7 个 URL**,且全是 `finance.sina.com.cn/stock/cpbd/`
|
||||
这类栏目首页,不是文章永久链接。读者点进去看不到对应报道。
|
||||
3. **零配图**。整页 0 个 `<img>`,信息密度高但没有视觉落点,移动端阅读疲劳。
|
||||
|
||||
## news002 的对应设计
|
||||
|
||||
### 1. 事件唯一化(结构性修复)
|
||||
|
||||
- 栏目分 `core` / `flex` 两档。`core` 必出;`flex` 只有拿到**未被使用过的独立事件**才输出,
|
||||
没料就整节删除,连同导航锚点一起去掉。**是事件决定栏目,不是栏目决定事件。**
|
||||
- 全页独立事件上限 `NEWS002_MAX_STORY_CARDS = 28`(news001 实际产出 49)。
|
||||
- 每张卡片必须带 `data-event-key="{事件指纹}"`,指纹 = 核心主体 + 核心动作,
|
||||
中英文报道共用同一指纹,全页不得重复。
|
||||
|
||||
### 2. 运行时去重闸门
|
||||
|
||||
prompt 层的「禁止重复」无法验证,所以在落盘后再做一次程序化清理:
|
||||
[`wechat-news-morning-dedup.mjs`](../wechat-news-morning-dedup.mjs),由 worker 在生成成功后调用。
|
||||
|
||||
判重优先级:
|
||||
|
||||
1. `data-event-key` 完全相同
|
||||
2. 来源 URL 归一化后相同 —— **仅当它是文章永久链接时**。
|
||||
`isArticlePermalink()` 会排除栏目首页/`index.*`,否则 news001 那种「7 个栏目首页」会把整页误删。
|
||||
3. 标题相似 —— `isSameNewsEvent()` 用字符二元组重合度(阈值 0.55)。
|
||||
仓库既有的 `isDuplicateNewsMorningStory()` 按空格分词,对中文标题基本失效,所以这里另做一层。
|
||||
|
||||
`history` / `knowledge` 不参与判重;`weather` / `brief` / `deepdive` 不会被删空。
|
||||
闸门只删卡片和空栏目,**不改写正文**,DOM 保持 div 配平。
|
||||
|
||||
### 3. 每条新闻配图
|
||||
|
||||
链路:SearXNG `img_src`/`thumbnail` → `normalizeSearchResult()` 透传 `image`/`publishedAt`
|
||||
→ 预取 brief 里以 `图:{URL}` 单独成行 → Agent 写进 `.card .media > img`。
|
||||
|
||||
- 有图:`loading="lazy"` + `referrerpolicy="no-referrer"` + `onerror` 降级为占位
|
||||
- 无图:`<div class="media ph">{栏目 emoji}</div>`,版式不塌
|
||||
- 禁止张冠李戴、禁止编造图片地址、禁止整页长图
|
||||
|
||||
### 4. 视觉与排版
|
||||
|
||||
| 维度 | news001 | news002 |
|
||||
|------|---------|---------|
|
||||
| 底色 | `#f7fafc` 冷灰 | `#FAF7F2` 暖纸 |
|
||||
| 主色 | `#b0191e` 正红 | `#C8362F` 墨红 + `#1B3A6B` 靛蓝 |
|
||||
| 页头 | 240px 渐变大色块 | ~160px 报头(masthead),首屏直接见内容 |
|
||||
| 标题字体 | 全黑体 | 标题衬线(Noto Serif SC),正文黑体 |
|
||||
| 导航 | 25 个锚点横向滚动 | 8 个主锚点 + 吸顶 |
|
||||
| 卡片 | 白底 + 左侧 3px 色条 | 图文混排,左侧 92px 4:3 缩略图 |
|
||||
| 深色模式 | 无 | `prefers-color-scheme: dark` |
|
||||
|
||||
CSS 固化在 `NEWS002_STYLE_BLOCK`,直接内嵌进 taskSpec,Agent 照抄即可,不依赖外部参照页。
|
||||
|
||||
> `.card .media` 的 `align-self:flex-start` 不能省。flex 默认 `stretch` 会把缩略图拉成整卡高度,
|
||||
> `aspect-ratio` 失效 —— 这个坑在首版预览里踩过。
|
||||
|
||||
### 5. 内容结构
|
||||
|
||||
- **一分钟速读**:6~10 条一句话,给只有 30 秒的读者
|
||||
- **深读一条**:当天最重要的一件事,300~450 字 + 一句金句,做差异化
|
||||
- **每条卡片的「为什么重要」**:一句话说清跟读者的关系。说不清就不写这条
|
||||
- **订阅引导块**:放在往期回顾之前
|
||||
|
||||
## 微信草稿兼容
|
||||
|
||||
[`wechat-daily-news-inline.mjs`](../wechat-daily-news-inline.mjs) 已同时识别两版 DOM:
|
||||
|
||||
| 元素 | news001 | news002 |
|
||||
|------|---------|---------|
|
||||
| 头条卡 | `.highlight-box*` | `a.lead-card` |
|
||||
| 日期行 | `.date-badge` | `.masthead .meta` |
|
||||
| 导语 | `.subtitle` | `.lede` |
|
||||
|
||||
**已知缺口**:news002 的 per-card 配图目前不会上传到微信正文。
|
||||
`uploadDailyNewsBodyImagesForWechat()` 只处理页面里已有的 `<img>`,而 inline 渲染的
|
||||
`renderListCard` / `renderFeatureCard` 不输出图片。公众号侧配图需要单独一轮改造。
|
||||
|
||||
## 预览与验证
|
||||
|
||||
```bash
|
||||
# 渲染一张 news002 静态预览页(样例数据,占位图)
|
||||
node scripts/preview-news002-template.mjs /tmp/news002-preview.html
|
||||
|
||||
# 相关单测
|
||||
node --test news-morning-templates.test.mjs \
|
||||
wechat-news-morning-dedup.test.mjs \
|
||||
wechat-news-morning-draft.test.mjs \
|
||||
wechat-news-morning-draft-worker.test.mjs \
|
||||
wechat-news-morning-search.test.mjs \
|
||||
wechat-daily-news-inline.test.mjs
|
||||
```
|
||||
Reference in New Issue
Block a user