Val Town: https://www.val.town/x/anmeimei/us-topnews-merge Base: https://anmeimei--1349d12aa7df11f1abc71607ee4eb77e.web.val.run
| 路由 | 内容 |
|---|---|
| / | CNBC Top + CBS Top 合并去重(重复保留 CNBC) |
| /politics | CNBC 政治与政策,扣掉已在 Top News 出现过的 |
| /economy | CNBC 经济,同上 |
| /tech | CNBC 科技,同上 |
源:
- CNBC Top https://www.cnbc.com/id/100003114/device/rss/rss.html
- CBS Top https://www.cbsnews.com/latest/rss/main
- Politics https://www.cnbc.com/id/10000113/device/rss/rss.html
- Economy https://www.cnbc.com/id/20910258/device/rss/rss.html
- Tech https://www.cnbc.com/id/19854910/device/rss/rss.html
参数(任何路由通用,多个参数用 & 连接,只有第一个用 ?): ?sim=0.45 阈值 ?tag=0 去来源前缀 ?diag=0 去诊断条目 ?debug=1 诊断网页
feed 第一条是「合并诊断」,置顶。guid 见文末 2026-09-04 那节(已改成不带日期)。
/ CNBC30 + CBS30 → 保留 56,丢弃 4 /politics CNBC30 → 保留 22,丢弃 8 /economy CNBC30 → 保留 28,丢弃 2 /tech CNBC30 → 保留 21,丢弃 9(9 条全部是「同一篇」精确命中)
板块 vs Top News 基本都是同一篇文章(链接相同),走精确匹配,不靠相似度猜。 跨 CBS/CNBC 才需要近似匹配。sim=0.36 会误杀 CBS 理财软文标题,故默认 0.45。
CBS 的 latest feed 会把 60 Minutes 旧片段按原始播出日期重发 (实测出现过同一条 "When Social Security mistakenly overpays benefits" 的 2025-07-27 / 2024-06-23 / 2023-12-20 三份)。 原来的近似判重有 WINDOW_HOURS=36 的时间窗,相隔几年的两条永远不会被拿来比较, 所以三条全留下了。
修法:在近似比较之前加一道「标题归一化后完全相同 → 同一篇」,这一步不看日期。 归一化 = 转小写 + 非字母数字全替换成空格 + trim。零误判风险。 本地用那三条真实标题跑过,2 条被丢、1 条保留,走的是 same-title 分支。
注意 CBS 这类旧稿是它自己有意重推的内容,不是脏数据,所以只去重、不按年龄过滤。
两处改动,均在 diagEntry():
-
日期按美东算,不再用 UTC。
new Date().toLocaleDateString("en-CA", { timeZone: "America/New_York" })之前用toISOString().slice(0,10),美东晚 8 点后 UTC 已经跳到第二天, 所以标题会显示 9/4 而实际是 9/3。 -
诊断条目的 guid 去掉日期:
diag-${base.pathname}。 每个 feed 永远只有唯一一条「合并诊断」,内容随刷新就地更新, 不再每天新增、不会积攒。
验证(2026-09-03 ET):
| 路由 | guid | 诊断条数 | item 总数 |
|---|---|---|---|
/ | diag-/ | 1 | 57 |
/politics | diag-/politics | 1 | 24 |
/economy | diag-/economy | 1 | 30 |
/tech | diag-/tech | 1 | 21 |
副作用:改 guid 会在 NetNewsWire 里各留下一条旧的孤儿诊断(四个 feed 各一条), NNW 会自行清掉。这是最后一次——以后 guid 固定,不会再产生孤儿。
问题:Tech / Politics / Economy 之间是否也有重复,要不要做成级联去重 (Tech 只扣 Top;Politics 扣 Top+Tech;Economy 扣 Top+Tech+Politics)。
实测(当天四个源各 30 条,按文章 URL 比对):
| 比对 | 重叠 |
|---|---|
| Tech ∩ Politics | 1 |
| Tech ∩ Economy | 0 |
| Politics ∩ Economy | 0 |
| Tech ∩ Top News | 10 |
| Politics ∩ Top News | 7 |
| Economy ∩ Top News | 1 |
那唯一 1 条(Musk 中期选举花钱)本身就在 Top News 里,两个板块已经各自扣掉了, 所以实际可见的跨板块重复是 0 条。 又把模糊阈值放宽到 0.40 并去掉 36 小时时间窗重跑,三个板块两两之间零个近似标题。
原因是结构性的:CNBC 每篇文章基本只归一个板块,Top News 是跨板块挑出来的一层覆盖, 所以「板块 ↔ Top」重叠大,「板块 ↔ 板块」几乎不重叠。
结论:不做级联。理由不只是没必要,还有代价—— 跨板块用 sim=0.45 会误杀(Economy 一篇关税报道 vs Politics 另一篇关税报道 是两篇不同文章,不是重复),且引入顺序依赖、/economy 每次多抓两个源。 以每天 0~1 条真重复换这些,不划算。
如果以后真的反复看到同一篇出现两次,再加只按 URL 精确匹配、不做模糊的级联, 那个零误杀风险。注意:这个结论基于一天的切片(90 条), Musk 那条证明「一篇挂两个板块」是可能的,只是罕见。
用户在 Top News 和 CNBC: Tech 里看到同一条新闻。查下来去重逻辑没坏 (当时 /tech 的诊断里就把它列为「同一篇」丢弃了),但暴露了两个成因:
成因一(竞态):CNBC 先把稿子发到板块,过一会儿才提拔进 Top News。 NetNewsWire 在这个空窗里抓了板块 feed,就存下一份;等它进了 Top News 又存一份。
成因二(结构性,更严重):CNBC Top News 只有 30 条、约 1.5 天翻一轮, 而板块 feed 能追到两三周前。用短窗口当参照系,绝大部分板块条目根本没被检查过。 2026-09-06 实测:
| feed | 时间跨度 | 比 Top News 窗口还老的条目 |
|---|---|---|
| Top News(参照系) | 09-04 11:00 → 09-06 00:24 | — |
| Tech | 09-02 12:43 → 09-04 14:47 | 21 / 26(81%) |
| Politics | 09-01 18:53 → 09-04 19:48 | 21 / 24(88%) |
| Economy | 08-19 21:29 → 09-04 17:16 | 25 / 26(96%) |
反过来:Top News 扣掉三个板块,板块恢复全量。
参照系(三个板块合起来 08-19 → 09-05,2.5 周)完整覆盖被过滤对象 (Top News 09-04 → 09-05,1.5 天),成因二的窟窿直接消失,不需要持久化存储。
实测 CNBC Top 30 条里 14 条(47%)已经在某个板块里,反转后 Top News 从 57 条降到 43 条。
一、Top News 扣板块只做精确链接 / 同标题匹配,不做相似度。 主 feed 误杀一条的代价远大于漏掉一条重复。CNBC 与 CBS 之间仍然用 sim=0.45。
二、板块条目压后 6 小时再输出(?delay=)。 等 CNBC 把该提拔的提拔完,★ 第一次渲染就准。 用户读完就标已读、不会回头看,所以「首次准确」比「事后补上」重要。 代价:板块新闻晚 6 小时出现,这段时间它在任何 feed 里都看不到。 (用户明确说不介意——他只用 NetNewsWire 每天 catch-up 一次,不做实时。)
三、板块里也上了 Top News 的条目标 ★(?star=0 关掉)。 反转之后,编辑部「这是今天头条」的信号会随着条目移出 Top News 而丢失, ★ 把这个信号保留在用户真正会读完的地方。
CNBC 偶尔先发 Top News、后发板块的话,那一刻 Top News 会显示它, 等板块拿到才扣掉 —— NNW 两边各存一份。治它只能延迟 Top News,不划算。 从实测看 CNBC 是先板块后头条(最近 12 小时属于板块的 5 条全都已在板块里),这个方向应该很少。
| 路由 | 条目 | 带 ★ |
|---|---|---|
/ | 43(丢弃 17) | — |
/tech | 30 | 4 |
/politics | 30 | 6 |
/economy | 29 | 3 |
?delay=24 时 /tech 变成 27 条,压后功能正常。
用户反馈:CBS Top News 每天推送太密集,而且「很多什么 9/11 的好多条,这些我确实不 care」。 CNBC 的密度刚刚好,但它基本只覆盖钱相关的事,缺 Lindsay Clancy 这类社会新闻。
CBS 的 feed 里有很大一部分条目链接是 /video/(电视节目片段),其余是
/news/(文字稿)。 两次抽样:主 feed 25 条里 11 条是 /video/;这次上线后 CBS
抓到 30 条里 16 条是 /video/——大约一半。
把 /video/ 条目和同一天的 /news/
条目配对,没有配上的残余基本都不是硬新闻:
| video-only 残余(抽样) | 类型 |
|---|---|
| 2 friends hit the road for a good cause | 软性栏目 |
| Exclusive discounts from CBS Mornings Deals | 带货广告 |
| Meet the psychiatrist behind "therapy speak" | 访谈片段 |
| Is quitting good for you? | 软性栏目 |
| American farmers sell herds over high costs | 节目片段 |
| 9/6: CBS Weekend News / 9/6: Face The Nation | 整期节目片头 |
| 9/6/2026: Sharswood; Boom Chicago | 地方台整期节目 |
| Connecticut school honors Sept. 11 attacks victim | 纪念类 |
| Lindsay Clancy 庭审片段 | 真新闻,唯一一条 |
硬新闻(飓风、空难、庭审)文字和视频两种格式都会有;video-only 的漏网基本是软内容。
唯一一条真新闻 Lindsay Clancy,在 cbsnews.com 上搜到大量 /news/ 文章
(/news/lindsay-clancy-mistrial-what-happens-next/、
/news/trump-lindsay-clancy-case-mistrial-hung-jury-horrible-thing/,还有若干
/boston/news/…),
说明这类连续跟进的大案子不会因为剔掉视频而丢——今天漏了明天还在。
用户自己的判断(「CBS 的 text 和 video 基本都讲一件事儿,真正重要的新闻两个 format 都会有」) 结论对,理由只对一半:不是每条视频都有对应文字,而是没有对应文字的那些本来就不值得读。
const isVideo = (it: Item) => /\/video\//.test(it.link || "");
collect() 加了 keepVideo 参数,默认剔除,?video=1 保留。 counts
统计的是过滤后的数量(所以诊断里 CBS 显示 14 而不是 30),
剔掉多少条单独写在诊断第一段:「剔除 CBS 电视片段 N 条」。
板块路由的 videoOut 只算板块自己的(永远是 0,CNBC 没有 video 链接),
不把「Top News 参照系剔掉的」算进去——那会让 /tech 的诊断显示一个跟它无关的数字。
| 路由 | 条目 | /video/ 链接 |
|---|---|---|
/ | 32(31 条新闻 + 诊断) | 0 |
/tech | 30 | 0(4 条带 ★) |
/politics | 29 | 0 |
/economy | 31 | 0 |
?video=1 恢复:Top News 41 条新闻,其中 11 条是 /video/。 四个路由都没有 NaN
/ undefined。
| 源 | 每天条数 |
|---|---|
| CBS Top News(未过滤) | ~30 |
| CNBC Top News | ~20 |
| PBS NewsHour | ~19 |
| NPR News | ~8 |
用户两三天没 catch-up,回来发现 US Top News 里混着 4 月 / 7 月 / 8 月的 CBS 文章, 而且清楚记得这几天反复标过已读。
该 feed 171 条未读,按到达日期分组:
| 到达日 | 条数 |
|---|---|
| 09-04 | 1 |
| 09-08 | 7 |
| 09-09 | 63 |
| 09-10 | 69 |
| 09-11 | 31 |
那 11 条"很久远"的条目,发布日期从 2026-04-30 到 08-29,到达日期全部是 09-09。
所以不是同步丢失,也不是标过又变回未读:CBS 在 09-09 往 feed 里吐了一批旧存档, NetNewsWire 按发布日期排序,它们一进来就沉到列表最底部,从上往下读永远扫不到。
佐证:09-11 当天 CBS 源里仍挂着一条发布日期 2024-08-01 的「The 60 Minutes
Channel」, 只是恰好是
/video/,被视频过滤器顺手挡掉了。这个源确实会往外吐旧条目。
Top News 的输出加发布时间上限,默认 10 天,?maxage= 可调,?maxage=0 不限。
只作用于 / 自己的条目——不作用于板块参照集,也不作用于板块路由。
为什么板块不加:Top News
是「现在在发生什么」,十天前的东西躺在列表底部对读者零价值。 板块不是。实测
09-11 当天 /economy 最老一条发布于 08-25(17 天前), 加 10
天上限会直接砍掉一大半;而且板块的时间深度还要拿去给 Top News 当去重参照系,
砍短了会让 2026-09-06 那次反转解决的窗口盲区重新出现。
| 路由 | 条目 | 最老发布日 | 备注 |
|---|---|---|---|
/ | 34 | 09-10 | 默认 maxage=10,当天没有超龄条目 |
/?maxage=1 | 33 | 09-10 20:28 | 剔除 1 条,诊断第一段有提示 |
/?maxage=2 | 34 | 09-10 01:26 | 剔除 0 条 |
/tech | 24 | 09-09 | 不受影响 |
/politics | 25 | 09-08 | 不受影响 |
/economy | 28 | 08-25 | 不受影响(这条就是不给板块加上限的理由) |
四个路由都没有 NaN / undefined。
Val Town 存盘时会跑 prettier,把长语句折成多行。用「整行匹配 +
在该行之后插入」的方式
改代码时,如果目标语句在线上已经被折成多行,插入点会落在语句中间、把它劈成两半——
这次就把 const delayH = 和它下面的三元表达式劈开了,线上直接 500。
改之前先把目标区域按行打印出来确认实际排版,不要假设它和本地文件长得一样。
- 财新滚动要闻 / 晨读荐闻:val
anmeimei/caixin-briefing-rss - 财新 T早报(tzaobao.ts)也在同一个 val 里
- MIT Tech Review 三个 AI feed 合并去重:val
anmeimei/mit-tr-ai