- news002 image/structure audit no longer throws: prune empty sections and publish a best-effort page with a warning - WeChat push strips images that failed to upload instead of refusing the draft - forceGenerateOnce is cleared before running, so a failed run cannot loop; the archived page is restored when generation fails - degraded-page regeneration runs at most once per day - content safety rules cover vulgar sexual terms and gossip/adult sites Co-authored-by: Cursor <cursoragent@cursor.com>
9.4 KiB
每日新闻早报模板:news001 / news002
模板定义集中在 news-morning-templates.mjs。
早报页由 Agent 按 spec 生成 HTML,仓库里没有静态页模板文件,所以「模板」= 交给 Agent 的版式与内容规范。
| 模板 | 版本 | 状态 | 说明 |
|---|---|---|---|
news001 |
2026-09-18-refined |
frozen | 2026-09-18 定稿版式,已上生产。只读,不允许就地修改;要改版式请新开 news003。 |
news002 |
2026-09-21-subscriber |
active | 订阅向改版:事件唯一化、每条配图、报头版式、深色模式。 |
切换模板
# 环境变量(默认 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 旁的分析脚本可复现):
- 重复选题。49 张新闻卡片里只有约 20 个独立事件。「王毅同鲁比奥通电话」出现 4 次, 「东博会南宁开幕」5 次,「OpenAI 霍奇猜想」5 次。 根因是版式把「23 个栏目 × 固定条数」当硬指标,当天独立事件不够时 Agent 就换个标题再写一遍。
- 来源不可核验。49 张卡片只引用了 7 个 URL,且全是
finance.sina.com.cn/stock/cpbd/这类栏目首页,不是文章永久链接。读者点进去看不到对应报道。 - 零配图。整页 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,由 worker 在生成成功后调用。
判重优先级:
data-event-key完全相同- 来源 URL 归一化后相同 —— 仅当它是文章永久链接时。
isArticlePermalink()会排除栏目首页/index.*,否则 news001 那种「7 个栏目首页」会把整页误删。 - 标题相似 ——
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" - 无图:先抓来源页
og:image/twitter:image;仍没有就不放配图,保留文字卡 - 禁止默认小图凑数、禁止张冠李戴、禁止编造图片地址、禁止整页长图
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 已同时识别两版 DOM:
| 元素 | news001 | news002 |
|---|---|---|
| 头条卡 | .highlight-box* |
a.lead-card |
| 日期行 | .date-badge |
.masthead .meta |
| 导语 | .subtitle |
.lede |
news002 的卡片图会先上传到微信正文素材接口,再由 inline 渲染器输出微信 CDN 地址。
静态页与微信草稿均使用 左上角 72~92px 小图(.card / .lead-card 左侧缩略图),不再用全宽大图。
能拿到真实配图的卡片必须显示并上传;没有真实图就只保留文字卡,不放默认小图。
远程配图会先下载到当日 MindSpace public/,改写成本站相对 URL,避免海外图源在国内打不开。
推送前不再删除无图故事卡(微信草稿保留纯文字卡);有图卡未能上传微信 CDN 时只去掉该图,卡片保留文字。
配图 / 结构 / 写满检查只引导修复、不阻断生成和推送:
检查不过时依次做 SearXNG 补位、补图、头条规整;仍不满足就删掉完全空的栏目,
以现有内容出稿并照常推草稿,同时记 news002 audit incomplete, publishing best-effort page 告警。
强制重生成(forceGenerateOnce)在执行前就清掉标记,只跑一次、失败不重试;
生成失败时把归档的原稿放回原位,当日页面不会因重生成失败而 404。
降级页自动重生成每天最多一次。
真实图片来源按优先级取 SearXNG 结果里的 img_src/thumbnail,以及文章来源的
og:image:secure_url、og:image、twitter:image;不使用与事件无法核验关系的泛图片搜索结果。
内容安全底线闸门
成人、未成年人保护、法律合规、人文伦理、来源资质是底线,只过滤、不阻断:命中就删卡或改写,生成和发布照常进行。
| 层 | 位置 | 做什么 |
|---|---|---|
| 提示词 | NEWS_MORNING_CONTENT_SAFETY_TASK_SPEC(拼进生成任务,不改冻结的 news001 模板) |
告诉 Agent 底线 |
| 检索 | wechat-news-morning-search.mjs |
SearXNG safesearch=2;预取结果先过 filterNewsSearchGroupsForSafety,因为 SearXNG 补位会把预取结果直接写进页面 |
| 页面 | wechat-news-morning-content-safety.mjs,worker 在去重后、配图检查后各跑一次 |
逐块检查卡片 / 速读 / 深读 / 导语;一次批量模型复审 |
| 推送 | pushDraft / 预览前 sanitizeNewsMorningHtmlSync |
只删 block 级,标题与摘要残留词打码 |
规则在 news-content-safety-rules.mjs,分两级:
block:命中必删,模型复审不能放行(色情、荐股、虚拟货币推广、赌博毒品交易、歧视词、八卦、灾难调侃、非新闻来源、自媒体作时政来源等)。review:交给模型复审(性犯罪新闻、涉未成年人伤害、自杀、恐袭、传闻、标题党、币价行情等)。复审不可用时按失败即删处理。
删卡导致栏目不满时,被删过内容的栏目会进 exemptSectionIds,配图/写满检查对这些栏目豁免,否则「过滤」会变成「阻断发布」。
被删事件出现在 description 里时,用剩余速读重建 description;残留在 meta、JSON-LD 的 block 级词打码。
页面 footer 与微信草稿底部追加「本文由 AI 辅助整理」显式标识,<head> 写入 memind-aigc(隐式标识)与 memind-content-safety(审计标记)。
- 关闭模型复审(规则仍生效):
MEMIND_NEWS_MORNING_CONTENT_REVIEW=0 - 复审模型 / 超时:
MEMIND_NEWS_MORNING_CONTENT_REVIEW_MODEL、MEMIND_NEWS_MORNING_CONTENT_REVIEW_TIMEOUT_MS(默认 90s)
规则只能加严或修正误伤,不得为了写满栏目放宽 block 级规则。修正误伤时必须在
news-content-safety-rules.test.mjs补一条「不应命中」的用例。
预览与验证
# 渲染一张 news002 静态预览页(样例数据,占位图)
node scripts/preview-news002-template.mjs /tmp/news002-preview.html
# 用当天真实新闻 + 对应文章来源图片生成视觉样图(不推微信、不写生产目录)
node scripts/build-news002-real-design-sample.mjs
# 相关单测
node --test news-morning-templates.test.mjs \
news-content-safety-rules.test.mjs \
wechat-news-morning-content-safety.test.mjs \
wechat-news-morning-images.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