📰 HOOP NOW · 新闻小程序 · 架构图纸(调研 + 设计)
图纸 · 未开工 2026-08-13 这一页由 docs/NEWS_PLATFORM.md 生成 —— 改内容改那份 md,别改这个 HTML。小程序平台本身归 Fable,这份不碰。

一眼看完

2
个类别
P0 先窄后宽
10
步流水线
每天跑一次
7
道审核闸门
闸门是代码,不是提示词
7
张表
从数据库到推送
$12
AI 文字处理/月
只是文字那块,不是整个产品
25
条待你拍板
其余我都定了

这套东西怎么切的

第一部分
🤖 AI 编辑部
读源文本 → 填一张表
填不出来填 null,绝不编
中间的契约
🧱 框架 JSON
headline / key_numbers /
entities / hero / news_type…
存进 doc jsonb
第二部分
🎨 排版
只认字段,永不猜
模板由代码按规则推

👆 Jeff 2026-08-13 定的切法。好处不是好看:换版式不用重跑 AI, 出错能定位到「哪个字段填错了」,闸门能逐字段查。

0. 一句话结论

能做,而且噱头不在「AI 写新闻」——那玩意儿满大街都是。 噱头在它长在聊天软件里(读完一条能直接甩进群里吵)和订阅制(你自己挑类别,不靠推荐算法猜)。

真正会决定生死的不是技术,是三件事:版权、配图、防幻觉。 这三件在下面各有一节。

0.1 这套东西到底是什么(Jeff 2026-08-13 定的形态)

Jeff 原话:「不要把它设计成『AI 帮新闻排版』,而是做成一个 AI News Studio + 群内新闻阅读器AI 负责当编辑,不负责乱画版面;排版引擎负责把 AI 的编辑决定变成漂亮且稳定的视觉。这两层一定要分开。

四句话说清它和「新闻 app」的区别:

普通新闻 app HOOP NOW
你在哪读 打开一个 app 就在群里,读完能直接吵
读多久 一篇长文 三层:3 秒 / 10 秒 / 为什么重要(§3.0)
一条新闻几张卡 一张 两张 —— 第二张是「为什么大家在讨论」(§3.5.3)
你能做什么 👍❤️😂 Explain / Ask AI / Discuss / Save / Follow(§3.9)

⚠️ Jeff 那句必须刻在墙上的话:「漂亮是第二重要,新闻不失真才是第一重要。」 他给的例子:Reuters 报「Anthropic 据消息人士称正在洽谈收购 Decart AI,规模约 60 亿美元」—— AI 不许把标题写成「Anthropic 60 亿美元收购 Decart」,因为交易还没完成。 「据报道 / 洽谈 / 已完成」这几级必须原样保住。这条已经做成闸门 7(§3.6),不是靠提示词求它。


2.1 噱头在哪 —— 老实说

不在这儿: 「AI 每天写新闻」。这个 2026 年已经不稀奇,Apple 都做了(还翻车了)。

在这两条:

  1. 读完能立刻吵起来。 今日头条要你看完再去别处聊;我们的新闻本身就在聊天软件里 —— 一条金价新闻,长按甩进群、直接开投票「买不买」。别人要复制这个,得先有一个 IM。
  2. 订阅,不是喂你。 头条靠推荐算法猜你想看,越猜越窄;我们是你自己挑类别,每天固定几条。 在信息过载的今天,「少而准」反而是卖点

⚠️ 一句实话:广度上打不过今日头条,别往那儿比。 我们赢在小、准、能社交。 Artifact 的教训就是:别做一个「又一个新闻 app」。

2.2 判据:什么算「高质量」(可测)

「高质量」不能停在形容词。定义成可测的:

指标 怎么量 及格线(建议,待拍板)
来源可溯率 有原文链接且链接可达的条数 / 总条数 100%,低于就不发
事实锚定率 每条里的具体数字/人名/时间,能在源文本里找到的比例 ≥ 95%
重复率 同一天同一类里,讲同一件事的条数 ≤ 1
时效 从源发布到我们推送的中位时长 < 6 小时
打开率 推送 → 打开的比例 建立基线后再定
投诉/纠错数 用户报错条数 / 周 单条纠错 1 小时内能下架

3.1 分类怎么切

⚠️ 这一节有一处 Jeff 前后给了两套,我不替他合并,摆出来等他定(见 §6 第 9 条)。

Jeff 08-13 最新那段给的是五类(HOOP NOW 进群后那一行):

类别 为什么
🌍 World 大盘子,人人有兴趣
💼 Business 源规整、事实性强,数字类最容易验证
🤖 AI 我们自己的读者最关心;Jeff 举的例子(Anthropic / Decart)就在这类
Sport 高频、情绪强,天生适合甩进群吵
🎬 Entertainment 用户面广

他更早那条语音点的是:财经、黄金、娱乐 + 我加的本地(马新)

我的建议(要他一句话):在五类基础上加第六类 🇲🇾 Local(马新)。 理由:World/Business/AI/Sport/Entertainment 这五类,Apple News 和 Google News 都有, 而且比我们全;唯独马新本地新闻大平台覆盖薄 —— 那才是我们真能赢的一块。 黄金可以并进 Business,不单开一类。

⚠️ 类别数直接乘成本(见 §4):五类比四类贵 25%,六类贵 50%。 但绝对值很小($12 → $18 一个月),这一刀该按产品定,不该按成本定

3.2 时区与节奏

  • 时区一律 Asia/Kuala_Lumpur(UTC+8),和新加坡同一时区,不做多时区。
  • 每类每天跑一次,清晨出稿。建议 06:30 生成 → 07:30 推送(赶上班前那一眼)。
  • ⚠️ 待拍板:财经/黄金要不要加一次收盘后(比如 18:00)。加了成本翻倍。

1. 调研:外面是怎么做的,以及谁摔死了

1.1 Artifact —— Instagram 创始人做的 AI 新闻 app,做死了

这是最值得看的一个,因为他们做的事和我们要做的几乎一模一样,而且人、钱、技术都不缺。

  • Kevin Systrom 2024 年 1 月宣布关停,app 2024 年 4 月正式下线。
  • 官方理由(Systrom 原话):"the market opportunity isn't big enough to warrant continued investment" —— 「我们做出了一个核心用户很爱的东西,但市场盘子不够大。」
  • 数字很难看:2023 年 2 月上线以来总下载 444,000,而且大部分集中在发布那几天,之后月下载量断崖。
  • 地域极度失衡:美国占 44%,其他任何一个国家都不超过 4%
  • 2024 年 3 月,Yahoo 收购了它的个性化/推荐技术,并入 Yahoo News。

这对我们的三条启示(这是本节最重要的部分):

  1. 「单独一个新闻 App」是个坏生意 —— 用户手机里已经有 Apple News / Google News, 再装一个次要新闻 app 的动力很弱。⚠️ 而我们不是要做单独的 app,我们是长在 HOOP 里的一个小程序 —— 这恰好绕开了 Artifact 的死因。这是我们和它最大的结构性差别,不是运气。
  2. 他们的技术值钱,产品不值钱 —— Yahoo 买的是推荐引擎,不是那个 app。 说明「AI 处理新闻」这件事本身有价值,但要挂在一个已经有用户的地方
  3. 留存比下载重要得多。 发布那几天冲一波然后掉光,是这类产品的标准死法。

来源: - TechCrunch — What happened to Artifact? - Mediagazer — Systrom 关停声明 - Failory — Why Artifact Failed - TechCrunch — Yahoo 收购 Artifact 技术

1.2 Apple 的翻车 —— AI 摘新闻编造标题,被 BBC 投诉到下架

这一节直接决定我们的「审核闸门」怎么设计,请当成必读。

Apple 在 iOS 18 的 Apple Intelligence 里加了「通知摘要」,把新闻 app 的推送自动缩写。结果:

  • 把 BBC 的推送摘成 「Luigi Mangione 开枪自杀」 —— 事实是他活着且在羁押中。
  • 摘出 「Luke Littler 赢得 PDC 世界飞镖锦标赛」 —— 比赛还没开始
  • 摘出 「Rafael Nadal 出柜」 —— 无中生有。
  • 把《纽约时报》三篇文章缩成一条推送,称 「以色列总理内塔尼亚胡被捕」 —— 没这回事。

结果:2025 年 1 月,Apple 在 iOS 18.3 beta 里把新闻/娱乐类 app 的 AI 摘要功能整个关掉。 是 Apple 这种体量的公司,做的还只是「把已有文字缩短」这种最保守的任务。

⚠️ 我们要做的比这难得多 —— Apple 只是,我们是。 Apple 用的还是原文做输入,而幻觉照样发生。

给我们的判据: - 「AI 只是摘要,所以不会编」是错的。 摘要照样编,而且编得像真的。 - 出事的成本不对称:一百条对的没人夸,一条编的能上新闻。 - 必须有「关掉」这个开关 —— Apple 的处理方式就是整个功能下架。我们要能做到同样的事(见 §5.5)。

来源: - CNN — Apple pulls AI-generated notifications for news - Axios — Apple pauses AI-generated news alerts - Daring Fireball — BBC 那几条的细节 - CBC — 报道汇总

1.3 配图 —— Unsplash / Pexels 到底能不能用

结论:Pexels 可以,Unsplash 有两个坑,而两家都有一个共同的大坑。

Pexels Unsplash
免费商用 ✅ 允许,不强制署名(但用 API 时其指引要求链回 Pexels) ✅ 授予不可撤销、非独占、全球范围的使用许可,可商用
用 API 时 指引要求链回 ⚠️ 署名是强制的,不是「鼓励」——写在 API 服务条款里
致命限制 ⚠️ 不得汇编图片去复制或与 Unsplash 竞争的服务

⚠️ 两家共同的大坑,而且这条对新闻最致命: 它们都不保证「肖像授权(model release)」。 只要图里有可辨认的真人,拿去做商业/宣传性质的使用,通常需要本人书面同意 —— 而这两家都不提供。

对新闻场景的实际含义: - 拿一张「路人甲在街上走」的图去配一条犯罪新闻 = 在暗示这个路人是嫌疑人。这是实打实的诉讼风险,不是理论风险。 - 所以:有人脸的图,一律不许自动配到新闻上。 这条要做成流水线里的硬闸门,不是靠提示词。

AI 生图当新闻配图,风险更大(这条是判断,不是查到的条文,标明): 真人肖像、灾难/战争场景、看起来像「现场照片」的合成图 —— 一旦被当成真实影像传播, 性质就从「配图不当」升级成「伪造新闻图片」。

⚠️ 建议(要 Jeff 拍板):新闻配图只用抽象/示意图(图表、色块、图标、纯风景), 永不生成或使用「看起来像新闻现场」的图。宁可难看,不可惹祸。

来源: - Pexels Terms of Service - Unsplash — 能否用于个人或商业项目 - LicenseOrg — Unsplash 署名在 API 下是强制的 - LicenseOrg — 免费图库的授权陷阱(含 model release)

1.4 版权:马来西亚 / 新加坡的实际法条

我们用户主要在马新,所以先查这两地,没有拿欧美的先例硬套。

马来西亚 —— 《Copyright Act 1987》(Act 332)。合理使用(fair dealing)看四个因素: 1. 使用的目的和性质(商业性 vs 非营利教育) 2. 被使用作品的性质 3. 使用部分的数量和实质性(相对于整部作品) 4. 对该作品潜在市场或价值的影响

新加坡 —— 新《Copyright Act 2021》,2021 年 11 月 21 日生效。 把这一条从 "fair dealing" 改名为 "fair use",并删掉了原来的第五个因素 (「能否以通常商业价格取得该作品」)。马新两地的合理使用四因素是同一套。

⚠️ 这对我们的含义,说实话: 四因素里,第 1 条(商业性)和第 4 条(是否影响原作市场)对我们都不利 —— 我们是商业产品,而且「让人不必去点原文」正是替代原作市场。

所以我的建议不是「靠合理使用抗辩」,而是根本别走到需要抗辩那一步: - 只取标题 + 极短事实性摘要,改写而非搬运; - 每条必须显著带原文链接和出处,把流量送回去而不是截留; - 绝不整篇转载,绝不做「不用点进去就看完」的长摘要。

⚠️ 这条我做不了法律结论,只能给出风险姿态。上线前建议找一次本地律师看条款 —— 这是要 Jeff 拍板的事。

来源: - Laws of Malaysia — Copyright Act 1987 (PDF) - Lexology — 新加坡新著作权法十大变化 - infojustice — Copyright Act 2021 的 fair use 修订 - Wikipedia — Copyright law of Malaysia

1.5 没查到 / 没查透的(诚实标注)

以下几项我没有查到可靠一手来源,不编:

  • AP / Reuters / AFP 的授权价位 —— 公开网页查不到报价,这类都是一对一谈。要报价得直接联系他们。
  • NewsAPI / GDELT 的商用条款细则 —— 没逐条读过它们的 ToS,不敢下结论。
  • AI 生图的单张成本 —— 我们自己那条生图管线的单价我没现查,§4 的成本表里留空标注
  • Semafor Signals / Channel 1 AI 的内部分工 —— 没找到可靠的一手说明。

1.6 🔥 法国报业刚告了 Google 的 AI 摘要(2026-08-11,两天前)

这条线索是 GPT 那份方案提的,我独立查证过 —— 属实,而且比它说的更有份量。

  • 2026 年 8 月 11 日,法国 APIG(代表约 300 家法国日报)向法国竞争管理局 正式投诉 Google 的 AI 摘要(AI Overviews,2026 年 7 月底开始出现在法国搜索结果里)。
  • 投诉的核心不是"抄了我的字",是"截了我的流量":法国出版商称 AI 摘要 让他们的引荐流量掉了 33%–38%
  • 欧盟委员会 2025 年 12 月已就「Google 用出版商内容做 AI Overviews / AI Mode 的条件是否不公平」 开了正式反垄断调查;法国竞争管理局 2026 年 7 月 8 日刚命令 Meta 就 AI 内容使用与出版商重启谈判, APIG 拿这个当先例。

⚠️ 这条对我们的直接含义,比 §1.4 那些法条更硬: 监管盯的不是"你有没有署名",而是"你有没有让用户不必去点原文"。 换句话说,做得越"读完就懂",法律风险越高 —— 这和产品直觉正好相反。

所以 §3.5.2 那个「点进去是一条 Story」必须留一个出口: 05 — Source 那一屏不是装饰,是把人送回原文的那扇门,而且要显眼。 三层阅读的第三层「为什么重要」是我们写的判断,不是原文的替代品 —— 这个区别要在版面上看得出来。

来源: - Euronews — 法国日报就 AI 摘要向竞争管理局投诉 - France 24 — French newspapers appeal to antitrust regulator - ppc.land — AI 摘要令流量下滑最多 38% - TechRepublic — French Publishers Challenge Google AI Search

1.7 ⚠️ NewsBreak —— 「抓全文 + AI 改写」这条路真有人走过,踩了什么雷

这条是 Jeff 08-13 提的线索,我查证过,而且实情比他说的更难看。

NewsBreak(美国下载量最高的新闻 app 之一,月活 5000 万+)的模式正好是我们要避开的那种: licensed content + 抓取本地新闻/新闻稿 + AI 改写。路透调查发现:

  • 2021 年以来至少 40 起错误或凭空虚构的内容。
  • 最难看的一条:AI 生成的报道称新泽西 Bridgeton 平安夜发生枪击 —— 当地警方公开声明整件事根本不存在,NewsBreak 几天后才撤。
  • 还有把食物银行发放时间写错、把一家非营利机构的服务描述错的,都是记者找上门才改。
  • 版权:2022 年 Patch Media 起诉 NewsBreak 未经许可、不署名转载其报道, 以 175 万美元和解(NewsBreak 称和解不代表承认过错)。

⚠️ 这个案子的三条,每条都能直接变成设计约束:

  1. 「5000 万月活」证明这条赛道商业上做得大 —— 方向没问题,问题在做法。
  2. 它虚构的不是观点,是「事件本身」 —— 一场根本没发生的枪击。 这正是 §3.11 Evidence Pack 存在的理由:先证明事件存在,再写它。 闸门查得再细,若一条新闻从头到尾只有一个来源,查的也只是「改写得像不像」。
  3. 175 万那笔和解买的教训是:「转载 + 不署名」比「写得不好」贵得多。 所以 §1.4 那三条(不整篇转载、显著带出处、把流量送回去)不是姿态,是省钱。

来源: - Engadget — Popular US news app accused of using AI to make up fake stories - The Standard — 报道汇总(含 Patch 175 万和解) - Foreign Press — NewsBreak Under Scrutiny

3.-1 🏛️ 六层架构(Jeff 2026-08-13 定,替换掉原来的 Sources → AI → News)

Jeff 原话:「现在的 Sources → AI → News 太简单了。真正应该升级成: HOOP Discovery Network → Event Graph → Evidence Engine → AI Newsroom → Design Engine → Distribution。 这已经不是『做一个漂亮的新闻卡片功能』了,而是在建一家 AI-native 新闻企业的基础设施。」

┌─ ① DISCOVERY ────────────────────────────────────────────────┐
│  RSS · Sitemap · 官方公告/IR · Spider · Free API · 搜索发现     │
│  产出:URL / 线索流                    ⚠️ 免费优先阶梯见 §3.10  │
└──────────────────────────┬───────────────────────────────────┘
                           ↓
┌─ ② EVENT GRAPH ──────────────────────────────────────────────┐
│  去重 · 聚类 · 实体识别 → 同一件事的多个来源收拢成「一个事件」   │
│  产出:event(不是 article)                                    │
└──────────────────────────┬───────────────────────────────────┘
                           ↓
┌─ ③ EVIDENCE ENGINE ──────────────────────────────────────────┐
│  多个 Agent 并行找证:官方公告 / 交易所文件 / 通讯社 / 背景       │
│  产出:Evidence Pack(§3.11)· ⚠️ 这一层没过就不许写稿           │
└──────────────────────────┬───────────────────────────────────┘
                           ↓
┌─ ④ AI NEWSROOM ──────────────────────────────────────────────┐
│  AI Journalist(写)→ AI Fact Check(查)→ AI Editor(定稿)      │
│  产出:框架 JSON(§3.0)+ 闸门报告(§3.6)                       │
│  ⚠️ 高风险类必须过人(§3.7)                                    │
└──────────────────────────┬───────────────────────────────────┘
                           ↓
┌─ ⑤ DESIGN ENGINE ────────────────────────────────────────────┐
│  AI 在 8 个母版闭集里选 → Renderer 精准执行(§3.5)              │
└──────────────────────────┬───────────────────────────────────┘
                           ↓
┌─ ⑥ DISTRIBUTION ─────────────────────────────────────────────┐
│  推送 / 群 / Ask AI / Discuss / Save(§3.9)                    │
└──────────────────────────────────────────────────────────────┘

⚠️ 这一刀真正的分水岭在 ② 和 ③,不在 ①。 「有没有 Spider」只是工程量差别;「以事件为单位、先攒证据再写稿」才是和 『抓一篇改写一篇』的结构性差别 —— NewsBreak 那场不存在的枪击(§1.7)就死在这里: 它抓的是一篇稿,所以没有任何一层能问一句「这事到底发生了没有」。

判据:我们发布的单位是「事件」,不是「文章」。 一个事件可以有 0 篇我们的稿(证据不够就不写),也可以有 1 篇 + 第二张卡。

3.10 🕷️ HOOP Discovery Network —— 免费优先的取源阶梯

📌 Jeff 08-14 要的 3.10A/B/C/D 四章长在这几节,这里只给指针(同一件事只能有一个出处): 3.10A 资源地图 → §3.14 · 3.10B HOOPBot 架构 → §3.15 · 3.10C Company Watch → §3.16 · 3.10D 权利登记表 → §3.17

Jeff:「Free first:RSS → Sitemap → 官方公告 → Spider → Free API → Search discovery → 最后才 Paid API / Licensed feed。」

# 来源 拿到什么 成本 法律位置
1 RSS 标题/摘要/链接 ~0 最干净 —— 人家主动发布给你订的
2 Sitemap 新 URL 发现 ~0 公开、为爬虫准备的
3 官方公告 / IR / 交易所文件 一手事实 ~0 最硬的证据,而且通常明确允许引用
4 Spider 全网线索 工程 + 带宽 ⚠️ 必须守 robots.txt,见下
5 Free API 结构化 ~0 看各家 ToS(§1.5 标了没查透)
6 搜索发现 补漏 只当线索,不当来源
7 Paid / Licensed 速度 + 法律确定性 💰 赚钱之后再买,当 premium verification layer

⚠️ 这个阶梯要加一条出口(2026-08-14 查证后补):遇到 HTTP 402 Payment Required 时, 这条源直接进「要谈 / 要付费」名单,不是重试。 Cloudflare 2025-07-01 已经上了 pay-per-crawl(见 §3.15.2)—— 「抓取」正在从免费的技术动作变成一笔交易。 排程器把 402 当成永久状态,不当成临时错误。

⚠️ 三条硬规矩(第 4 步那个 Spider 才是有风险的一步):

  1. robots.txt 是硬约束,不是建议。 今日头条自己就公开写明媒体可以用 robots.txt 拒绝 ToutiaoSpider 收录。 我们的 Spider 必须有名有姓(带 UA 和一个说明页)、遵守 disallow限速。 判据:一家媒体想拒绝我们,要有一个不用找律师就能做到的办法。
  2. 抓回来的全文只进「证据库」,永不直接进发布。 全文用来核对事实(闸门 2/3/7 要拿它 grep),不用来生成句子。 这条是 §1.7 那 175 万和解的直接对策。
  3. HTML → 结构化 JSON 别自己硬写解析器。 已经有专门做这件事的模型:news-crawler-LM(arXiv 2607.21284,2026 年 7 月)—— 小的长上下文模型,把新闻网页 HTML 转成正文和结构化 JSON(headline / author / 发布时间 / 正文), 在 HTML→JSON 任务上比强基线高 +2.2 BLEU / +4.1 METEOR。 这条是 Jeff 提的线索,我查证过,论文真实存在。arXiv:2607.21284

3.11 🧾 Evidence Pack —— 先证明事件存在,再写它

Jeff 给的例子:Spider 在某公司 IR 页看到「Company A announces acquisition of Company B for \$4.2B」—— 不要马上写新闻。 派 Agent 去找:A 的公告、B 的公告、交易所/监管文件、通讯社报道、背景资料。 「最后产生的不是『一篇被抄下来的文章』,而是一个 Evidence Pack。」

{
  "event_id": "uuid",
  "event": "Company A to acquire Company B",
  "status": "announced",              // rumored | reported_talks | announced | agreed | completed
  "first_seen_at": "…", "last_update_at": "…",
  "claims": [                          // ⚠️ 每一条事实单列,各自挂证据
    {"key":"deal_value", "value":"$4.2B",
     "evidence_ids":["ev_1","ev_3"], "conflict": false}
  ],
  "evidence": [
    {"id":"ev_1", "tier":"primary",  "kind":"company_announcement",
     "publisher":"Company A IR", "url":"…", "fetched_at":"…", "quote":"…原文里那一句…"},
    {"id":"ev_2", "tier":"primary",  "kind":"exchange_filing", "…":"…"},
    {"id":"ev_3", "tier":"wire",     "kind":"news_report", "publisher":"Reuters", "…":"…"}
  ],
  "independent_sources": 3,            // 互相独立的来源数(同一集团的算一个)
  "has_primary_source": true,          // 有没有一手件(公告/文件)
  "conflicts": [],                     // 来源之间打架的地方,**打架就不许发**
  "confidence_tier": "high"            // ⚠️ 见下:用档,不用 0.98
}

⚠️ 三条,其中第一条是我和原方案不同的地方:

  1. confidence 不用 0.98 这种小数,用「档」+ 可数的事实。 Jeff 08-13 刚让我把 importance8.7/10 改成 1-5,理由是模型给不出稳定的 8.6 vs 8.70.98 是同一个毛病,而且更危险 —— 它看起来像概率,会被当成概率去卡阈值。 改成:high / medium / low,由可数的事实推出来(几个独立来源、有没有一手件、有没有冲突), 不是模型自评(和 §3.0 硬规矩 ③ 同一个道理)。
  2. claims 要一条条挂证据,不能整包挂。 「交易金额」和「是否已完成」可能一个有一手件、 一个只有传闻 —— 整包一个 confidence 会把弱的那条也镀成金。
  3. conflicts 非空 = 不许发。 来源打架时,正确动作是降级成「多方说法不一」, 不是挑一个顺眼的写。

3.12 🗺️ 终局:HOOP Global Event Graph(现在不做,但地基别挖歪)

Jeff:「三年以后……50,000 个 source profiles、500,000 个 company/entity profiles、 每天 1,000,000 个 URL、每天聚类成 30,000 个事件、每个事件都有 evidence graph —— 那时候我们拥有的已经不只是一个 News App,而是一个 HOOP Global Event Graph。 新闻只是其中一个应用。」后面能长出:Company Intelligence / Investment Intelligence / AI Search / Personal Briefing / Industry Monitoring / Ask HOOP。

我同意这是这套东西真正值钱的地方 —— 但要说两句实话:

  1. 它是第二个产品,不是这个产品的 P0。 Jeff 自己 08-13 刚定了顺序:「先做 Renderer,视觉不成立后面全白搭」。 那条仍然对。 Event Graph 别在 P0-A 点头之前开工。
  2. 但地基现在就别挖歪 —— 有两个决定是「以后想改要重来」的,现在就得做对: - 发布单位是「事件」不是「文章」(§3.-1)—— 表结构上 news_events 要先于 news_articles 存在; - 每条 claim 挂证据(§3.11)—— 事后想补是补不回来的,因为源页面会变、会删。

判据:哪些东西「事后补不回来」,哪些「以后再做也不迟」? 前者(事件为单位、证据留痕、原文快照)现在就要;后者(实体图谱、跨事件关联、对外 API)等。

3.14 🗺️ Source Atlas —— 新闻资源地图(Jeff 2026-08-14:这是网站现在明显缺的一章)

Jeff 原话:「我们不能只写 Reuters、AP、RSS。真正应该建立一个 Source Registry / Source Atlas, 把整个世界的新闻资源分层……news_sources 甚至还只有 'rss' | 'api' 这种级别, 这已经跟我们『做自己的 AI 媒体公司』这个方向不匹配了。」

他点得对,而且这一章的骨架不是「列谁」,是他随口带过的那两句: Astro Awani 和 CNA 的 RSS 写明是个人、非商业用途这意味着「能不能抓」和「能不能用」是两件事 —— 这一章整个围绕这个展开。

3.14.1 ⚠️ 发现 ≠ 使用(这一章的核心,做成两个字段)

字段 意思 典型
use_for_discovery 拿它知道「发生了这件事」,然后去找一手件 CNA / Astro Awani / 各家媒体
use_for_content 文字/图片能不能进我们的产品 只有条款明确允许的才给 ✅

媒体源的正确用法是「线人」,不是「供稿方」: 它告诉我们去哪儿看,我们要写的那几句去 Bursa / BNM / 公司公告里拿

判据:这条源关掉之后,我们那篇稿还写得出来吗? 写不出来 → 说明我们在拿它当供稿方,那就得有授权。

⚠️ 这两个字段不分开,迟早有人「顺手」把媒体的句子放进产品里 —— 而那正是 §1.7 那 175 万和解的形状。

3.14.2 每个源一份 profile(在 Jeff 那版上加了四样)

{
  "name": "Bank Negara Malaysia",
  "country": "MY",
  "source_class": "primary",        // primary | wire | media | aggregator
  "source_type": "regulator",       // regulator | exchange | government | company_ir | news
  "methods": ["html", "rss"],
  "trust_tier": "A",                // ⚠️ 和 source_class 分开,见下
  "crawl_frequency": "10m",
  "languages": ["en", "ms"],
  "topics": ["finance", "economy", "banking"],

  // —— 下面四样是我加的 ——
  "use_for_discovery": true,        // 能不能拿来发现线索
  "use_for_content": true,          // 文字能不能进产品(§3.14.1)
  "commercial_policy": "allowed",   // allowed | non_commercial_only | unknown
                                    // ⚠️ unknown **一律按不可商用处理**(默认拒绝,不是默认允许)
  "robots": {"status": "allowed", "checked_at": "2026-08-14", "recheck_every": "30d"},
                                    // ⚠️ robots.txt 会变;抓一次就当永远允许是错的
  "policy_evidence_url": "…"        // 条款那一句在哪 —— 没有链接就不许标 allowed
}

⚠️ source_classtrust_tier 必须分开,Jeff 这一点分得对,而且正好接上 §3.13.2: 一手件 ≠ 可信。 监管机构是一手且可信(A); 公司 IR 也是一手,但它是利益相关方 —— 它说的话要按 claimed_by 渲染(带主语),不能直接陈述。 两个字段合并,这个区别就没了。

3.14.3 第一批:马来西亚 + 新加坡

Jeff 的判断我同意,而且这是我们真正的护城河:一手源(监管/交易所/部委/公司 IR) 能让我们比部分媒体更早发现线索 —— 因为媒体也是从这些地方拿的。

国家 class 先拿它做什么
🇲🇾 MY Wires Bernama(国家通讯社) wire 发现
General Media The Star · NST · Astro Awani · Malaysiakini · Malay Mail media 只发现
Business Media The Edge Malaysia media 只发现
Regulators Bank Negara Malaysia · Securities Commission primary 发现 + 内容
Exchange Bursa Malaysia(上市公司公告) primary 发现 + 内容
Government 财政部等各部会 siaran media primary 发现 + 内容
Companies 各公司 IR / Newsroom / Press Kit primary 发现 + 内容(⚠️ claimed_by)
🇸🇬 SG Media CNA · The Straits Times · Business Times · Mothership media 只发现
Exchange SGX Company Announcements primary 发现 + 内容
Regulator MAS(media releases / 货币政策) primary 发现 + 内容
Government MOF · MTI · MOM · MOH newsroom primary 发现 + 内容
Companies 公司 IR primary 发现 + 内容(⚠️ claimed_by)

🚨 诚实标注(这一条很重要,别跳过): 上表里所有源的 commercial_policy 现在一律是 unknown,也就是「按不可商用处理」。 Jeff 提到 Astro Awani 和 CNA 的 RSS 写明个人、非商业用途 —— 这个说法很可能是对的, 但我没有查到那一句条款的一手出处(搜索没搜到,CNA 的站点我这边抓不下来)。 所以我不把它当成已知事实写进表里。

这正好是我上面那条规矩的第一次应用:unknown = 默认拒绝。 下一步该有人逐条去读这些站点的 ToS,把 policy_evidence_url 填上 —— 填不上就永远停在 unknown,只能用于发现。 这件事没有捷径,也不该由 AI 猜。

3.14.4 news_sources 那张表要跟着升级

Jeff 点名的:「news_sources 甚至还只有 'rss' | 'api' 这种级别」。,§3.3 那张表按上面这份 profile 加字段:source_class / source_type / trust_tier / use_for_discovery / use_for_content / commercial_policy / policy_evidence_url / robots_status / robots_checked_at / crawl_frequency / languages[] / topics[]

⚠️ 别把这些塞进一个 jsonb 就完事 —— commercial_policyuse_for_content 是每次发布都要查的闸门条件,得是能加索引、能写约束的列。

3.15 🤖 HOOPBot 架构 —— 做 good bot,不做 stealth bot(Jeff 2026-08-14 第二块)

Jeff 原话:「目的不能是『这个网站挡我,我换 IP 绕过去』…… 正确架构是:多台 crawler worker + 固定、可识别的出口 IP 池 + 每个 domain 独立限速。」 「HOOPBot 最好做成 good bot,而不是 stealth bot。」

我完全同意,而且理由比「合规」更实在:固定出口 IP + 可识别 UA 是一项资产,不是限制 —— 它让媒体网站能把我们 whitelist藏起来的爬虫永远只能被当对手,而我们将来是要跟这些媒体谈授权的 —— 藏起来的人上不了谈判桌。

                HOOPBOT CONTROL PLANE
                        │
                Crawl Scheduler(全局限速在这一层)
                        │
        ┌───────────────┼───────────────┐
        ↓               ↓               ↓
   Worker MY        Worker SG        Worker US/EU
        │               │               │
    Egress IP        Egress IP        Egress IP
      Pool A            Pool B            Pool C
        └──────────── Internet ──────────┘

公开身份(照 Google 那套做):

User-Agent: HOOPBot/1.0 (+https://hoopcomm.com/bot)

/bot 那一页要有:我们是谁 · 为什么抓 · 联系方式 · opt-out · IP 段。 公开 IP 段的用处是让对方能用 reverse DNS + IP range 验证「这真的是 HOOPBot」—— Google 就是这么让站长验它的爬虫的。

3.15.1 为什么要多个 IP(四个理由,没有一个是「绕封锁」)

# 理由
1 吞吐 —— 几万个站点,一台机器顺序爬不完
2 地域 —— 马新/美/欧节点降延迟,也照顾区域性站点
3 可靠性 —— 一个机房挂了,不至于整个 Discovery Network 停摆
4 身份 —— 固定 CIDR 反而让媒体能 whitelist 我们

⚠️ 但不是每次请求随机换 IP。同一个 domain 保持稳定出口。 「被封 → 换住宅代理 → 继续偷偷爬」这条路法律、信誉、技术风险三样一起变大,我们不走。

3.15.2 robots.txt:标准是 RFC 9309,但它不是门锁

Robots Exclusion Protocol 已经是正式标准:RFC 9309(IETF), 规定了语法、优先级、出错怎么处理、怎么缓存。

第一次遇到一个 domain → GET /robots.txt → 解析 → 允许? ──YES→ 进排程
                                                    └─NO─→ STOP
缓存 robots,不是每篇新闻都重抓一次

⚠️ 两条必须知道的现实(查证过,不是推测): 1. RFC 9309 没有定义 AI 专用指令,而且遵守是自愿的、不构成访问控制 —— 换句话说:robots.txt 是「人家挂出来的牌子」,不是锁。守它是我们的选择,而这个选择就是我们的立场。 2. Cloudflare 已经在做 AI Crawl Control:对新站点默认拦截很多 AI 爬虫, 并且 2025-07-01 上了 pay-per-crawl —— 用 HTTP 402 Payment Required 让站点向 AI 公司收费。 ↳ Cloudflare 官方博客

⚠️ 这条对成本模型有直接影响:「抓取」正在从「免费的技术动作」变成「一笔可能要付钱的交易」。 §3.10 那个「免费优先阶梯」要加一级:遇到 402 = 这条源进「要谈/要付费」名单,不是重试。

3.15.3 四种 bot,但共用一个限速器

Bot 干什么
DiscoveryBot 找新 URL(RSS / sitemap / 列表页)
FetchBot 抓允许访问的具体页面
SourceHuntBot 发现事件后主动去找第二、第三来源(§3.11)
MonitorBot 盯重要公司/监管/交易所页面的变化

🔴 这是我要加的一条,而且是很常见的坑:四种 bot 必须共用同一个限速器和 robots 缓存。 否则四个 bot 各自都很守规矩,加起来把人家网站打挂 —— per-domain 的限速必须是全局的,不是 per-bot 的。这一条要写进调度器,不是写进规范。

每个 domain 的参数: per_domain_rate_limit · concurrency_limit · retry_backoff · robots_cache · error_budget

🟠 error_budget 超了要有动作,不能只记日志: 自动降频 → 停这个 domain → 报警。不然它只是个好看的指标。

3.15.4 一个真实场景(Jeff 举的,我照收)

Bursa 某公司出重大公告
  → MonitorBot 发现页面变化
  → FetchBot 取回公告原文
  → Event Engine 判断这是一起 acquisition
  → SourceHuntBot 自动去找:该公司 IR、另一方公司、SC、Bernama、The Edge
  → Evidence Pack(§3.11)
  → AI Journalist(§3.13)

这条链路里,媒体(Bernama / The Edge)是「印证」,不是「稿源」 —— 正好落在 §3.14.1 那一刀上。

3.16 🏢 Company Watch Network —— 先有名单,再谈爬

Jeff:「企业新闻不能靠 Spider 漫无目的爬整个 Internet。我们先有一个名单。

名单(第一批): 🇲🇾 Bursa 主板 + ACE 市场 + 主要非上市公司 · 🇸🇬 SGX 主板 + Catalist + 主要非上市公司 · 🌏 S&P 500 · Nasdaq 100 · 恒生 · 日经 · Fortune Global 500 · 主要 startup

每家公司建一份「地址簿」:

company.com                官网
company.com/newsroom       新闻室
company.com/investor       投资者关系
company.com/press          新闻稿
company.com/blog           博客
交易所代码                  ← 用来跟 Bursa/SGX 公告对上
监管备案页                  ← SC / MAS
CEO / 关键人物              ← 用来做 entity 关联

然后 MonitorBot 定向盯这些地方。 这种数据比全网乱爬质量高太多,而且量可控、成本可算。

🟠 我要加的一条:这份名单会腐,而且腐了没人会发现。 公司会退市、更名、被并购;IR 页会改版换 URL。所以每条要有: - last_verified_at —— 上次确认这个地址还活着是什么时候 - 自动失效检测 —— 连续 N 次 404 / 页面结构变了 → 标记 needs_review,从排程里摘出来

判据:一份名单如果没有「它什么时候会被发现是错的」这个机制,它三个月后就是一份烂名单。

3.17 📜 Source Policy / Rights Registry —— 权利登记表(五项分开)

Jeff 要的:「每个源能不能 crawl、能不能保存全文、能不能用图、RSS 是否商业可用、需不需要 licence。」

他分得比我细,我采纳。 我原来只有 use_for_discovery / use_for_content 两个布尔 —— 不够,因为「能不能保存全文」和「能不能用它的文字」根本是两件事: 前者是留证(内部),后者是发布(对外)。

# 权利项 问的是 默认
1 may_crawl robots + ToS 允不允许我们抓 ❌ 直到确认
2 may_store_fulltext 能不能把原文存进证据库(内部留证)
3 may_use_text 文字能不能进产品(对外发布)
4 may_use_images 图片能不能用
5 rss_commercial_ok 它的 RSS 商业可用吗 ❌(见 §3.14.3)
6 licence_required 要不要谈授权 / 付费 ⚠️ 未知即视为「要」

三条规矩: 1. 全部默认 ❌ —— 「没查过」和「不允许」在系统里是同一个值(§3.14.2 的 unknown 就是这么用的)。 2. 每一项 ✅ 都要有 policy_evidence_url —— 指到条款那一句。没有链接的 ✅ 不许存在。 3. 这张表是发布前的闸门条件,不是文档 —— 它得是能加索引、能写约束的列, may_use_text = false 的源,它的句子在代码层就进不了 body_blocks (和 §3.13.3 那句「programmer 根本没有机会不小心发出去」同一个做法。)

3.18 🔭 ScoutBot —— 让 Source Atlas 自己长大(Jeff 2026-08-14)

Jeff:「谁来不断扩大 Source Atlas?不能以后 programmer 手工添加 50,000 个网站。 应该再有一个 ScoutBot —— 它不是抓新闻,而是寻找新的新闻源。」

发现 company.com
  ↓ 探这几个位置
  /press · /news · /newsroom · /investor · /blog
  /rss.xml · /feed · /sitemap.xml · /robots.txt
  ↓ 识别官方社交账号 → 判断语言 → 识别所属国家
  ↓
提出 Source Profile(§3.14.2)
  ↓
Policy Check(§3.17)
  ↓
加入 Source Atlas

🔴 这里有一条后门,必须现在堵(我加的)

ScoutBot 自动扩 Atlas,而 Atlas 决定谁能进产品 —— 那它就是一条绕过权利登记表(§3.17)的后门:一个自动加进来的源, 可能带着某个默认值就进了发布流水线。

堵法:ScoutBot 只有提名权,没有录用权。

它能做 它不能做
发现新源、探测端点、填 profile 的事实字段(语言/国家/有没有 RSS/robots 怎么写) 动任何一个权利字段
把源加进 Atlas,状态一律 discovery_only commercial_policyunknown 改成别的
标记「这个源看起来值得升级」 升级它自己(要人过一眼)

它可以自动长到 5 万个源 —— 但那 5 万个默认全是「只能用来发现」。 判据同 §3.14.2:「没查过」和「不允许」在系统里必须是同一个值。

3.19 📊 Source Reputation Engine —— trust_tier 靠积累,不靠人写

Jeff:「Trust Tier 以后不能主要靠人工写……应该慢慢让系统积累自己的历史。」 而且要分领域:「有些媒体体育很准,财经不一定强。」

每个源滚动 12 个月的成绩单:

指标 说明
发现事件数 它先报了多少件
后来被证实 其中多少件被一手件/多方证实
重大纠正 / 撤回 这两个是负分的大头
平均领先时间 比别家早多少分钟(这是它的真本事)
原创事件率 有多少是它自己挖的,不是跟发的(接 §3.20 Lineage)
被引用次数 别家引它多少

分领域打分(politics / tech / finance / sports 各一档),不是一个总分。

🟡 冷启动:第一天我们一条历史都没有(我加的)

必须写清两件,否则这套引擎第一天就转不动: 1. 初始值从哪来 —— 用 source_class先验: 监管 / 交易所 / 公司 IR 起步就是 A(它们是一手件),通讯社 B+,一般媒体 B,自媒体 C。 2. 积累到多少条之后,track record 才允许盖过先验 —— 建议某个领域满 100 条已判定的事件才开始用实测分,之前只用先验。

不写死这条,新源要么永远进不来,要么一进来就被当 A。

3.20 🧬 Source Lineage —— 「谁先说的?」(这四个里最值钱的一个)

Jeff:「Reuters 独家 → Site A → Site B → Site C, AI 不能看成 4 个来源,实际上只有 1 个 original source。

{
  "origin_source": "Reuters",     // 谁先说的
  "derived_sources": 3,           // 跟发的有几家
  "independent_sources": 1        // ⚠️ 闸门用的是这个数,不是总条数
}

为什么我说它最值钱:我们所有闸门都依赖「几个独立来源」这个数字。 如果那 4 个其实是 1 个,credibility、confidence、单源孤证判定全是假的 —— 而且是看起来很扎实的假。

怎么判:

信号 能不能自动化 备注
canonical link ✅ 能 最硬的一条 —— 它自己声明了原文在哪
发布时间顺序 ✅ 能 谁先谁后
引用关系(文中写「据路透社」) ✅ 能 正则 + 出处名单
相同独特措辞 ⚠️ 只当辅助 见下

⚠️ 「相同措辞」这条要小心(我加的)

通讯社供稿本来就是逐字转载(syndication)—— 那不是抄袭,是授权转载。 所以不能拿措辞相似度去指控谁但对「独立来源计数」来说效果完全一样:它们只能算一个。

判据不是「它有没有抄」,是「它有没有自己去核过」。 落地:措辞相似度只用来降低 independent_sources 计数, 不用来给源扣信誉分(§3.19)—— 两件事别混。

3.21 ⚡ 两条管道:Daily Brief + Live(Jeff 说的最大缺口)

Jeff:「如果我们真的自称 Newsroom,就不能『上午 10 点 Nvidia 突然宣布重大收购 → 明天早上才报道』。」

                      EVENT
                        │
          ┌─────────────┴─────────────┐
          ↓                           ↓
    DAILY PIPELINE               LIVE PIPELINE
    每天精选 / 早报                  7×24 Monitor
    06:30 生成 → 07:30 推送             ↓
                                  Evidence Engine
                                       ↓
                                    Risk Gate
                                       ↓
                                    Publish

🟠 但这里和闸门直接打架 —— 我的解法要 Jeff 点头

冲突是真的: - Jeff 说 importance = 5 才进实时 —— 但 importance 是 AI 判的; - 而 §3.7 已经把 Breaking 列为「必须过人」。 - 实时要快,过人要等。两条规矩对着干。

我的解法:Live 管道的门槛不是「重要」,是「有没有一手件」。

进 Live(不必等第二来源) 走 Daily + 人工
公司公告 / 交易所披露 现场、灾害的目击描述
监管机构公告(BNM / SC / MAS) 传闻、爆料、匿名消息人士
官方赛果 犯罪指控、伤亡人数
公司 IR 的正式发布 任何 conflicts 非空的(§3.11)

理由:这几类不需要等第二来源,因为一手件本身就是来源 —— Bursa 的公告不需要 Bernama 来印证。

判据一句话:我们只在能证明的地方快。 这样「快」和「不失真」就不打架了 —— 而不是靠调一个 importance 阈值去赌。

⚠️ 另外:Live 也要有节流 —— 同一个用户、同一类,每小时最多推 N 条(N 待定), 否则就是 Jeff 说的「用户会被新闻轰炸」。

3.22 🖥️ HOOP MEDIA OS —— 一张总图(Jeff 2026-08-14)

                    HOOP MEDIA OS
                       INTERNET
                          ↓
                      SCOUTBOT          §3.18  找新的源
                          ↓
                    SOURCE ATLAS        §3.14  资源地图
                          ↓
             ┌── HOOPBOT NETWORK ──┐    §3.15  四种 bot,共用限速
             │                      │
          Monitor               Discovery
             └──────────┬───────────┘
                        ↓
                   EVENT ENGINE        §3.-1 ②  以事件为单位
                        ↓
                  SOURCE LINEAGE       §3.20  谁先说的
                        ↓
                 EVIDENCE ENGINE       §3.11  先证据后写稿
                        ↓
                REPUTATION ENGINE      §3.19  源的成绩单
                        ↓
                   RISK ENGINE         §3.13.3  GREEN/YELLOW/RED
                        ↓
                  AI NEWSROOM          §3.13  十道工序
                        ↓
                  DESIGN ENGINE        §3.5   八种母版
                        ↓
           ┌────────────┴────────────┐
        LIVE NOW                 DAILY BRIEF   §3.21
           ↓                         ↓
              HOOP CHAT / GROUPS     §3.9

Jeff 的原话,值得留在这儿当这份图纸的分界线:

「你这次更新最大的进步,就是它已经从『AI 新闻小程序设计文档』, 开始变成一间机器化媒体公司的 operating system 蓝图。」

⚠️ 他同时定了下一步:「补完这四个就应该开始做两个 prototype,而不是继续无限设计。」 这四章就是最后四章 —— 设计到此为止。

3.13 📰 AI Newsroom —— 十道工序 + claim 级留痕(Jeff 2026-08-14 定)

Jeff 原话:「不是一开始就假装我们已经是 Reuters,而是一开始就按照一家真正媒体公司的生产流程建设。 前期新闻少完全没关系,甚至我认为少一点更好……每天 50 条高质量,都比每天机器灌进 5,000 条不可靠内容好。

Spider / RSS / Official Feed
   ↓  ① DISCOVERY      发现一个可能的新闻事件
   ↓  ② SOURCE HUNT    自动寻找更多独立来源
   ↓  ③ EVIDENCE       保存事实 + 来源 + 时间(§3.11)
   ↓  ④ RISK SCORE     判断能不能自动出版 → GREEN / YELLOW / RED
   ↓  ⑤ AI JOURNALIST  HOOP 自己写(以 claim 为单位,见下)
   ↓  ⑥ FACT CHECK     逐句检查依据
   ↓  ⑦ AI EDITOR      标题 / 摘要 / Why it matters
   ↓  ⑧ IMAGE RIGHTS   确认图片使用权(图片也分三级)
   ↓  ⑨ DESIGN         Breaking / Big Story / Data …(§3.5)
   ↓  ⑩ PUBLISH        → HOOP

⚠️ 每一道工序都要能回答一句话:「它拦住了什么?」 媒体公司的流程是给设计的,照搬到机器上很容易做出一堆没人看的中间产物。 判据:这一道有没有否决权? 比如 ⑦ AI EDITOR —— 如果它只是把标题重写得漂亮点,那它是成本;它得有权在「标题和 claims 对不上」时打回,才算一道工序。

3.13.1 🧬 claim-level provenance —— 每一句话都知道自己从哪来

Jeff:「每一句话从哪里来的……未来如果有人问『HOOP,你这个数字哪里来的?』, 后台不是 AI 回忆,而是真的拿得出证据。这才开始像媒体机构。」

我认为这是这一整轮里最有价值的一条,因为它把「防幻觉」从事后检查变成结构上做不到。 它比 §3.11 的 Evidence Pack 细一格:那一层是「事件 → 事实 → 证据」,这一层是「句子 → claim → 证据」。

"claims": [
  {"id":"c01", "text":"NVIDIA announced a next-gen AI chip on Thursday",
   "attribution":"fact",                    // ⚠️ 见 3.13.2
   "evidence_id":"ev_1", "tier":"primary",  // NVIDIA Official Newsroom
   "as_of":"2026-08-14"},
  {"id":"c02", "text":"The company expects shipments to …",
   "attribution":"claimed_by", "speaker":"NVIDIA",
   "evidence_id":"ev_2", "tier":"primary"}, // earnings release
  {"id":"c03", "text":"Analysts expect …",
   "attribution":"claimed_by", "speaker":"分析师(某某)",
   "evidence_id":"ev_5", "tier":"secondary"}  // ⚠️ 二手
],
"body_blocks":[
  {"type":"para", "claim_ids":["c01","c02"]}   // ⚠️ 正文块**引用 claim**,不自己写字
]

⚠️ 一个实现坑,必须现在说清楚:句子和 claim 不是一一对应。 一句话可能含两个 claim,一个 claim 也可能跨两句。 所以不能事后做「句子 → 来源」的映射(那是猜的)—— 必须让 AI 写作时就以 claim 为单位输出,再由 Renderer 拼成句子。 判据:body_blocks 里不许出现没有 claim_ids 的正文。

3.13.2 ⚠️ 一手件 ≠ 事实(我加的一条,原方案没有)

公司公告、IR、财报是一手件 —— 但它同时是「利益相关方的自我陈述」。 「NVIDIA 预计出货会很好」是一手件,但不是事实,是「公司说」。

GREEN 自动发布这一类时,如果版面把它写成陈述句, 我们就变成了企业公关的免费发布通道 —— 而且是自动的、每天的。

所以框架里加 attribution:

什么意思 渲染时
fact 可独立核验的事实(赛果、已公布的价格、监管文件里的数字) 可以直接陈述
claimed_by 谁说的(公司预期、分析师看法、当事人声明) 强制带主语:「NVIDIA says…」

判据:去掉主语之后,这句话还成立吗? 不成立 → claimed_by,渲染时不许省主语。

3.13.3 🚦 自动出版三级(内容) + 三级(图片)

Jeff:「初期我们甚至可以只发布 GREEN。」图片那三级他的原话是: 「这样 programmer 根本没有机会『不小心』把侵权照片发布出去。」

这句话是这套设计里最好的一句 —— 用架构挡,不是用纪律挡:RED 的素材 Renderer 根本拿不到

内容 图片
🟢 GREEN
自动发布
公司公告 / 财报 / 政府与监管公告 / 产品发布 / 明确的比赛结果 公司 Press Kit 明确允许 editorial use、自有图、明确许可的图库、AI 示意图
🟡 YELLOW
AI 写完必须人批
并购传闻 / 裁员 / 政治 / 敏感商业消息 / 重大突发 授权条件需要人工确认
🔴 RED
暂时不做
严重指控 / 未经证实的死亡 / 匿名爆料 / 高法律风险 / 来源互相矛盾 从新闻网站抓来的 Reuters/Getty/AP 照片(没有 license)—— Renderer 直接拿不到

⚠️ 第一阶段先做这几类(Jeff 给的清单,共同点是「有一手件可对、版权风险低」): 企业新闻(Newsroom / IR / 财报 / 公告)· 科技与产品(发布会 / 开发者 Blog)· 金融市场数据(合法来源的指数、业绩、公开 filings)· 政府与监管(部门 / 监管机构 / 交易所)· 体育结果 · 研究与科学(论文、大学与研究机构公告)。

先暂缓: 战争现场 · 犯罪指控 · 政治爆料 · 匿名消息人士 · 名誉风险高的报道 · 无法独立验证的 Breaking · 没有合法图片来源的事件。

⚠️ 两条我要补在这上面:

  1. 体育结果放 GREEN 要小心 —— 赛果本身是事实,但赛果数据常常是有主的(体育数据商授权)。 Jeff 自己写了「在数据和图片权利明确的前提下」——那个前提要落成一道检查,不能只是备注
  2. 「跑三个月再开 YELLOW」建议改成数字,不是日历。 三个月但没测过错误率,一样不能开。出闸条件建议写成: 某一类 GREEN 连续 N 条零纠错 + 随机抽检 M 条人工复核全对 → 才开这一类的 YELLOW。 (N/M 待定,§6)用可测的,不用时间。

3.0 🧱 新闻框架 —— AI 编辑部和排版之间的唯一契约(Jeff 2026-08-13 定的第一步)

Jeff 原话:「新闻分两部分:AI 负责编辑,定新闻的框架,框架用 JSON 存…… 这个框架是我们第一步要做的。」

这一刀比「AI 写文章 → 排版渲染文章」狠得多,值得先说清好在哪:

「AI 写一篇稿子」 「AI 填一张表」(Jeff 这版)
排版怎么知道重点 得去哪句是重点 重点是字段,不用猜
换版式 要重跑 AI 不用碰 AI,同一份 JSON 换个模板重渲
出错怎么定位 「这篇写坏了」 key_numbers[1] 填错了」
闸门怎么查 拿正则去捞文章里的数字 逐字段查,查得动(§3.6)
双语 翻译整篇 只翻三个字段

判据:排版层永远不许去读 body 猜结构。 它要的每样东西,框架里都得有一个字段。

⚠️ importance 我建议用 1-5 整数,不用 8.7/10(原方案给的刻度)。 模型给不出稳定的 8.7 和 8.6 的差别 —— 换个 prompt、换个批次就变。 假精度的坏处不是"不够准",是你会拿它去排序,而它其实是噪音。 1-5 整数(或者干脆 breaking / big / normal 三档)反而稳。这条待 Jeff 拍板(§6 第 12 条)。

框架长什么样(v1)

{
  "schema_version": 1,              // 框架会变;旧稿要还能渲染,靠这个
  "id": "uuid",
  "run_date": "2026-08-14",
  "category": "finance",            // finance | gold | entertainment | local
  "lang": "en",                     // 每种语言一份 doc,不在一份里塞两套

  // —— 这条是什么新闻 ——
  "news_type": "big_story",         // **内容语义**(这是什么新闻),不是模板名
  "importance": 4,                  // ⚠️ 见下方「假精度」那条:建议 1-5 整数,不用 8.7/10
  "status": "developing",           // developing | confirmed —— 版面上要显示
  "fact_level": "reported_talks",   // ⚠️ 事实层级,见 §3.6 闸门 7,**不许被升级**

  // —— 三层阅读(Jeff 08-13 加的,这是产品的核心手感) ——
  "layer1_3s":  "Anthropic 据报洽谈约 US$6B 收购 Decart AI",     // 3 秒看到 · ≤ 60 字符
  "layer2_10s": "Claude 开发商 Anthropic 正在讨论收购 Nvidia 支持的 AI startup Decart AI。若交易达成,规模可能约 60 亿美元。",
  "layer3_why": "如果交易完成,意味着 Anthropic 正进一步扩张 AI 基础设施和技术能力。",
  // ⚠️ layer3_why 是**我们写的判断,不是媒体原文** —— 卡面上必须标 `AI CONTEXT / AI 解读`
  //    (Jeff 08-13:别等到 Ask AI 才区分,从新闻卡本身就开始区分)
  "subheadline": "Reportedly in talks to acquire Decart AI",     // ≤ 90 字符,可为 null

  "body_blocks": [                  // 点进去那条 Story 的正文;列表页只用上面三层
    {"type": "para",   "text": "……"},
    {"type": "bullet", "items": ["……", "……"]},
    {"type": "quote",  "text": "……", "speaker": "Anwar Ibrahim", "source_ref": 0}
  ],

  // —— 第二张卡(Jeff 08-13 加:同一则新闻,AI 自动创造第二张) ——
  "second_card": {                  // 可为 null;不硬凑
    "kind": "why_talking",          // why_talking | what_it_means | bull_vs_bear
    "title": "WHY PEOPLE ARE TALKING ABOUT THIS",
    "points": ["……", "……"],
    "bull": null, "bear": null      // kind = bull_vs_bear 时才填这两个
  },

  // —— 事实锚点(防幻觉的主力) ——
  "key_numbers": [
    {"label": "Gold spot", "value": 2401.50, "unit": "USD/oz",
     "change": 1.8, "change_unit": "%", "direction": "up",
     "as_of": "2026-08-14T09:00:00+08:00", "source_ref": 0}
  ],
  "entities": [
    {"name": "Bank Negara Malaysia", "type": "org"},   // person|org|place|ticker|event
    {"name": "Kuala Lumpur", "type": "place"}
  ],
  "location": {"country": "MY", "city": "Kuala Lumpur", "scope": "national"},

  // —— 判断类(必须和事实类分开存) ——
  "sentiment":   {"tone": "positive", "score": 0.62, "subject": "gold market"},
  "credibility": {"score": 88, "source_count": 3, "top_tier": true,
                  "corroborated": true, "method": "computed_v1"},

  // —— 版面 ——
  "hero": {"kind": "chart", "chart": {"series": "XAUUSD", "window": "1M"},
           "image_url": null, "credit": null, "alt": "Gold price, past month"},
  "layout": {                       // **视觉决定**,和 news_type 分开(Jeff 08-13 纠正)
    "template": "big_story",        // AI 在 8 个母版闭集里选
    "variant": "B"                  // ⚠️ A/B/C 由 **Renderer** 按图片/人脸位置/标题长度/数字多少挑,不归 AI
  },


  // —— 出处与留证 ——
  "sources": [
    {"publisher": "Reuters", "url": "https://…", "published_at": "…", "item_id": "uuid"}
  ],
  "gates": {"…": "闸门逐项结果,见 §3.6"}
}

news_type 全表(这一栏决定版式)

⚠️ 这里我改过一次,Jeff 08-13 纠的。 我原来把 news_type 和模板名合成一个, 理由是「别维护两套词」。他的原话:「出发点没错,但未来会限制设计」—— 同一条 big_story,以后可能有 A/B/C 三种视觉,news_type 就是模板名的话会慢慢混乱。

现在的分法:news_type = 内容语义(这是什么新闻);layout.template + layout.variant = 视觉决定。 名字故意取成一样的(big_storybig_story),所以还是一套词、不用对照表; 但它们是两个字段,variantRenderer 挑(按图片、人脸位置、标题长度、关键数字多少)。 判据:AI 说「这是什么新闻」,Renderer 说「这一条长什么样」。

什么时候用 它必须有 MVP
breaking 突发 / 重大事件 locationentities ≥ 1
big_story 重要 / 影响深远 key_numbers ≥ 1 或 hero
normal 日常 / 一般性新闻
data 含数据 / 讲趋势 key_numbers ≥ 1
explainer 需要解释 / 背景知识 body_blocks 里 ≥ 3 段
quote 名人发言 / 官方声明 body_blocks 里有 quote
timeline 事件发展过程 / 时间线 timeline[] ≥ 3 项
multi 简报 / 多条新闻一览 引用 ≥ 3 条别的 article

⚠️ MVP 那一栏是 Jeff 定的:「我甚至不会先做 100 种新闻。先做 Breaking / Big Story / Normal / Data / Explainer 五种,把这五种做到非常漂亮,再让 AI 自动选择。」

⚠️ 三条硬规矩(这三条是这份框架真正值钱的地方)

① AI 在闭集里选版式,代码验它选得合不合法。

⚠️ 这一条我改过。 我原来写的是「版式由代码推出来,AI 一个字都不许决定」。 Jeff 08-13 那段说的是「AI 判断新闻 → AI 选择视觉语言 → 排版引擎生成, 这才会看起来像 AI 在设计」「AI 负责审美决定,Renderer 负责精准执行」。 他是对的,而我那条太紧了 —— 全靠规则推,出来的东西一辈子长一个样, 那就不是「AI 在设计」,是「模板套娃」。

改成两半,各管各的: - AI 有审美决定权,但只能在 §3.5 那 8 个母版里选一个(闭集,不许自由发挥画图)。 - 代码当守门员:选中的母版声明了它要哪些字段(见 §3.0 news_type 全表), 字段不齐 → 驳回,自动降到能满足的那个;全不满足 → 落 normal(兜底,不许没有)。 驳回要记进 gate_report,别静默改掉 —— 静默降级会让人以为 AI 选得很准。

判据:AI 挑的是「哪种视觉语言」,不是「这里字号多大、留白多少」。 后者永远归 Renderer —— 那才是「今天很漂亮明天很丑」的根源(Jeff 原话)。

② 填不出来就填 null,绝不许编。 框架里没有一个字段是"必须编一个出来"的(除了 headline/summary)。 key_numbers 里每个数字必须带 source_ref,指向 sources 里的第几条 —— 指不出出处的数字,不许进框架。这一条把「事实锚定率 ≥ 95%」(§2.2)从口号变成可查的。

credibility 不许让写稿那个模型自己打。 自己给自己打分永远是高分。P0 用的(method: "computed_v1"): 源的层级(通讯社/官媒/自媒体)× 印证条数(几家独立报了同一件事)× 有没有官方一手来源。 要上模型打分,也得是另一个调用、看不到写稿过程。

存哪儿

news_articles 的正文部分整份存 jsonb,不再拆成 headline/body 两列:

news_articles
  …
  doc            jsonb           -- ⬅️ 上面那份框架,整份存
  schema_version int             -- 冗余出来做索引和迁移
  headline       text GENERATED  -- 从 doc 生成,只为了列表页查得快
  news_type      text GENERATED
  importance     int  GENERATED
  INDEX (category_key, run_date, importance DESC)

为什么 headline 用 generated 而不是各存一份: 手抄一份就会腐(铁律 23)。 真相只有 doc 一处,索引列是它的投影,不是副本。

⚠️ 待 Jeff 拍板(这份框架本身): 上面这些字段够不够、有没有多余的。我建议 v1 就到这里,先跑起来再加 —— 字段一旦发出去就有旧稿在用它,加容易删难。

3.5 News Design System —— 八种母版(Jeff 08-13 出了版式库图)

Jeff 原话:「我反而不建议每一则新闻都叫 AI 自由发挥画一张图。因为那会发生一个问题: 今天很漂亮,明天很丑;这一篇像杂志,下一篇像海报;整个 HOOP 里面完全没有品牌语言。 应该建立一个 News Design System:只给 AI 6-8 种非常高级的母版,但每种母版内部可以自动变化。」

「一套 template 重复 100 次」和「AI 每次自由发挥」都不对。 中间那条路是: 母版是闭集(8 个),母版内部按内容自动变化(字号、图 crop、留白、数字位置这些归 Renderer)。

# 母版 什么新闻用它 版面长什么样 MVP
01 BREAKING 突发 / 重大事件 超大标题 + 强视觉,红标
02 BIG STORY 重要 / 影响深远 大图 + Headline + Key Fact 三小格
03 NORMAL 日常 / 一般性 左图右文 + 简短摘要
04 DATA 含数据 / 讲趋势 大数字 + 图表
05 EXPLAINER 需要解释 / 背景 3 张解释卡(①②③)
06 QUOTE 名人发言 / 官方声明 人物照 + 引号 + 一句话
07 TIMELINE 事件发展过程 时间轴
08 MULTI-NEWS 简报 / 多条一览 2×2 瀑布流「今日要闻速览」

MVP 先做前五种,做到非常漂亮(Jeff 定的),后三种留到 P2。

📐 母版必须按聊天气泡的真实宽度设计,不是按网页宽度

app/lib/screens/chat_screen.dart 量的真实约束(不是估的): 图文气泡 屏宽 × 0.76,纯媒体气泡 屏宽 × 0.86

机型 图文气泡最宽 纯媒体气泡最宽
iPhone 16 Pro Max 334 pt 378 pt
iPhone 15/16 标准 299 pt 338 pt
iPhone SE / mini 285 pt ← 按这个设计 322 pt

⚠️ 判据:新闻卡要在 285 pt 宽也不崩。 拿「展示网页」的尺寸做出来的母版, 塞进聊天窗口一定要重做一遍 —— 大数字会撑破、三栏会挤成一坨。 Data 母版那个「大数字」尤其危险:$6B 没事,RM 1,234,567 就要换行策略。

3.5A 📐 Visual Grammar —— 所有母版共同遵守的视觉语言

Jeff 2026-08-14:「programmer 看完还是会问:超大到底多大?图片占多少?标题最多几行? 三行怎么办?……这才叫真正的 Design System。

⚠️ 下面每个数字后面都标了「它是从第几张崩出来的」 —— 编号指 Golden Test Gallery 里那 30 例。 指不出编号的数字,我没写进来 —— 那种就是拍脑袋。

依据
卡片宽度 285–334 pt(按 285 设计) chat_screen.dart 量的(§3.5)
圆角 18 pt
外边距 14 pt(正文区左右)
基准栅格 4 pt
标题(headline) 19/25,Bold,最多 3 行,超出截断 #02 5 行把 hero 图整个盖死
副标题 13.5/20,最多 2 行 #12 署名挤成第三行像正文
正文(layer2) 13.5/22,最多 4 行 #28 正文很长时卡片被撑到两屏
AI 解读(layer3) 13/20,最多 4 行 #16 绿框吃掉半张卡
出处 / 时间 11.5/14,单行,截断加省略号 #19 超长出处名把来源行撑成两行
hero 高度 普通 132 pt;Breaking 不低于卡高 45% #02 标题压到 hero 只剩一条边
key_numbers 最多 3 个,多的丢详情页 #10 五个 chips 换行成两行

3.5B 🔤 Typography —— 三条,不是一张字号表

  1. 同一张卡最多三级字阶(标题 / 正文 / 元信息)。第四级一出现,卡就开始像海报。
  2. 中英混排不调字号,调行高 —— 中文 1.32,英文 1.25(#13 #14 #15 三张对比出来的)。
  3. 数字用等宽数字(font-variant-numeric: tabular-nums)—— 否则 7,798.9931,208.77 并排时数字宽度不一,列对不齐(#09)。

3.5C 🖼️ Image Composition Engine —— #23 证明它是必需项,不是加分项

Jeff 给的字段:subject / face_box / focal_point / safe_text_area / brightness

#23(亮图 + 白字几乎看不清)是我事先没预测到、gallery 自己崩出来的, 它把这一节从「以后再说」提到了 MVP 必做:

brightness > 0.6  → 文字区强制加暗渐变(不是可选)
face_box 在右半   → 文字排左;在左半 → 文字排右      (#22)
图片复杂度高      → 不 overlay 文字,改上下结构
没有 hero         → 直接落无图版式,不留占位灰块      (#05 #06)

⚠️ brightness 这一项必须在配图入库时就算好存下来 —— 渲染时才算就晚了(要等图下载完)。

3.5D 🧩 Template Contracts —— Big Story A/B/C(Jeff 画的,我照收)

variant 什么时候选 长什么样
A — Hero 有漂亮横图(hero_quality 高) 大图 → CATEGORY → 大标题 → 副标 → 数字条
B — Portrait 图里有人脸(CEO / 政治人物 / 明星) 左文右图,KEY FACT 压在左下
C — Number 数字本身就是新闻,图弱 巨大数字 → 标题 → 小图

挑选顺序(Renderer 判,不归 AI):有人脸 → B · hero_quality 高 → A · key_number 强 + 图弱 → C · 都不满足 → A

Breaking 的契约(Jeff 写的规则,我原样收): hero 占 55–65% · 标题最多 3 行 · BREAKING 标永远左上 · 只允许一个强调色 · 不显示 summary · 不显示 key fact 条 · source 永远在底部 · developing 必须出现

3.5E 📏 Responsive:285 → 334 之间只准变两样

只准变:hero 高度、正文行数。 不准变:字号、圆角、间距、栅格。 理由:字号一跟着宽度变,两台手机上的同一条新闻就不是同一个东西了, 而 golden 也没法比(#01–#30 两档各渲一次就是为了盯这个)。

3.5F 🚧 Overflow / Fallback —— 这一节整节都是崩出来的

情况 规则 依据
超长不可断单词 所有文本容器 overflow-wrap: anywhere #04 直接溢出被裁(我没预测到)
超长数字 ≤4 字符 → 40pt;5–7 → 34pt;8–10 → 28pt;>10 → 紧凑记号(RM1.23B) #08 RM 1,234,567,891 换行成两行
没有 hero 落无图版式,不留灰占位 #05 #06
没有副标题 / 没有 AI 解读 整块不渲染,不留空白 #17 #27
没有图表数据 只出大数字,不画空坐标 #29
未知 news_type normal 兜底 #26 已验证:没白屏
字段全空 只出标题 + 出处,卡片不塌 #30

3.5G ✨ Motion —— 100–200ms,只做三下

Jeff:「image fade → headline reveal → key number pop……不是花里胡哨,是让它感觉活着。

只做这三下,而且只在卡片首次进入视口时做一次。 三条纪律: 1. 总时长 ≤ 200ms —— 超过就从「活」变成「慢」; 2. 重渲不重播(切宽度、切主题不许重放动画); 3. prefers-reduced-motion 必须尊重 —— 关掉动画,卡片要一样完整。

3.5H 🖼️ Golden Test Gallery —— 已经做出来了,不是待办

docs/news_cards_gallery.html → 线上 /gallery 30 条极端案例 × 两档宽度,/cards 共用同一套 renderCard()

它已经抓到 7 处真崩点,其中 2 处(#04 #23)是我事先没预测到的 —— 这就是「先渲染再写规格」的全部理由:上面 3.5A~3.5G 里的数字,一半是它逼出来的。

⚠️ 验收规矩:改 Renderer 之后,30 张全部重渲一次;30 张全过,才算改完。

3.5.1 为什么必须 LLM → Design JSON → Renderer,而不是 LLM → PNG

Jeff:「你的绘图软件拿这些数据,自动决定字号、字重、图片 crop、留白、渐变、卡片高度、 数字位置、logo 位置。AI 负责审美决定,Renderer 负责精准执行。

LLM → 直接出 PNG LLM → Design JSON → Renderer
稳定性 每张都在赌 同样输入永远同样版面
改品牌色/字体 全部重画 改 Renderer 一处,历史全变
能不能测 不能 能 golden(版式坏了是代码 bug)
成本 每张都要生图 几乎为零
出错 「这张丑」 key_number 太长撑破了」

3.5.2 点进去是一条 Story,不是一篇文章

Jeff 给的骨架(封面卡只是入口):

01 — What happened     一句话
02 — The number        $6B   ← 一个巨大数字
03 — Who is Decart?    小解释
04 — Why it matters    AI 解读
05 — Source            Reuters 原始报道

这五段直接对上框架里的字段:layer1_3s / key_numbers[0] / body_blocks / layer3_why / sources[0] —— 没有一段需要 Renderer 去猜

3.5.3 第二张卡(Jeff 08-13 加的,我原来完全没有)

「同一则新闻,AI 自动创造第二张卡。第一张是 WHAT HAPPENED, 第二张不是重复新闻,而是 WHY PEOPLE ARE TALKING ABOUT THIS / WHAT THIS MEANS / BULL vs BEAR。这样『新闻』开始变成一种可以刷的内容产品。」

框架里对应 second_card(§3.0)。三条实现纪律:

  • 允许为 null —— 没话说就别硬凑第二张,凑出来的就是废话卡。
  • bull_vs_bear 只给财经/市场类,而且正反两面都要有 source_ref —— 否则它就是「AI 的看法」,那是评论不是新闻。
  • 第二张卡同样要过闸门(§3.6):它更容易编,因为它天生是「解读」。

⚠️ bull_vs_bear 有合规风险,原方案没提到,这里补上(2026-08-13 我评审时发现): 自动生成「看多 vs 看空」就是在给投资意见,马新对投资建议有牌照门槛。三条硬要求: 1. 正反两面都必须有 source_ref —— 是「谁说的」,不是「AI 觉得」; 2. 卡上硬标「非投资建议」,不是藏在设置里的免责声明; 3. 不许出现买/卖倾向动词(建议买入/应该减仓/值得上车…)—— 做成词表闸门,和闸门 7 一个做法。

判据:我们是「转述别人的多空看法」,不是「我们看多」。 这两件事在法律上不是一回事。

表关系图

👇 这张图不是画的,是从下面那段 schema 里解析出来生成的 —— 加一张表、改一个 FK,图会自己跟着变(手抄一张就会腐,铁律 23)。绿箭头 = 外键指向,虚线框 = 外部表。

source_id category_key article_id item_id user_id category_key user_id article_id news_categories key text PK name_en text name_zh text sort_order int enabled bool default true news_sources id uuid PK name text kind text url text category_key text publisher text enabled bool default true last_ok_at timestamptz last_error text created_at timestamptz users 外部表(不是这套的) news_articles id uuid PK category_key text FK run_date date doc jsonb schema_version int headline text GENERATED news_type text GENERATED importance int GENERATED status text gate_report jsonb model_id text token_in int token_out int created_at timestamptz published_at timestamptz NULL retracted_at timestamptz NULL retract_reason text NULL INDEX (category_key, run_date, importance DE news_items id uuid PK source_id uuid FK external_id text title text summary text url text published_at timestamptz fetched_at timestamptz cluster_id uuid NULL UNIQUE (source_id, external_id) news_subscriptions user_id uuid FK category_key text FK created_at timestamptz PRIMARY KEY (user_id, category_key) news_article_sources article_id uuid FK item_id uuid FK PRIMARY KEY (article_id, item_id) news_deliveries id uuid PK user_id uuid FK article_id uuid FK delivered_at timestamptz opened_at timestamptz NULL UNIQUE (user_id, article_id) -- ⚠️ 这个唯一

3.3 数据表(字段级)

HOOP 用 Postgres + UUIDv7。迁移号要从 backend/migrations/LEDGER.md 取一个,取完立刻 +1 写回 —— 本图纸阶段不占号、不写迁移文件

news_sources          -- 我们订了哪些源
  id             uuid PK
  name           text            -- 「Reuters 商品」
  kind           text            -- 'rss' | 'api'
  url            text
  category_key   text            -- 冗余:这个源主要喂哪一类
  publisher      text            -- 出处署名,发到用户面前的那个名字
  enabled        bool  default true
  last_ok_at     timestamptz     -- 上次成功抓取;监控用
  last_error     text
  created_at     timestamptz

news_categories       -- 类别
  key            text PK         -- 'finance' | 'gold' | 'entertainment' | 'local'
  name_en        text
  name_zh        text
  sort_order     int
  enabled        bool default true

news_items            -- 抓回来的原始条目(未加工)
  id             uuid PK
  source_id      uuid FK -> news_sources
  external_id    text            -- 源自己的 guid;去重用
  title          text
  summary        text            -- 源给的摘要,不是我们写的
  url            text            -- 原文链接
  published_at   timestamptz
  fetched_at     timestamptz
  cluster_id     uuid NULL       -- 聚类后回填:同一事件的多条指向同一个
  UNIQUE (source_id, external_id)

news_articles         -- 我们生成的稿件(全平台共享,不分用户)
  id             uuid PK
  category_key   text FK -> news_categories
  run_date       date            -- 哪一天那一跑
  doc            jsonb           -- ⬅️ §3.0 那份新闻框架,整份存;正文的唯一真相
  schema_version int             -- 冗余出来做索引和迁移
  headline       text GENERATED  -- 从 doc 投影,只为列表页查得快,不是副本
  news_type      text GENERATED
  importance     int  GENERATED
  status         text            -- 'draft'|'passed'|'blocked'|'published'|'retracted'
  gate_report    jsonb           -- 闸门逐项结果,留证
  model_id       text            -- 哪个模型写的
  token_in       int
  token_out      int
  created_at     timestamptz
  published_at   timestamptz NULL
  retracted_at   timestamptz NULL
  retract_reason text NULL
  INDEX (category_key, run_date, importance DESC)

news_article_sources  -- 一条稿件用了哪些源(多对多),防幻觉的锚
  article_id     uuid FK -> news_articles
  item_id        uuid FK -> news_items
  PRIMARY KEY (article_id, item_id)

news_subscriptions    -- 谁订了哪一类
  user_id        uuid FK -> users
  category_key   text FK -> news_categories
  created_at     timestamptz
  PRIMARY KEY (user_id, category_key)

news_deliveries       -- 推送记录(幂等 + 可回溯)
  id             uuid PK
  user_id        uuid FK -> users
  article_id     uuid FK -> news_articles
  delivered_at   timestamptz
  opened_at      timestamptz NULL
  UNIQUE (user_id, article_id)      -- ⚠️ 这个唯一键就是防重复推送的全部保证

⚠️ 两个设计要点,别改: 1. news_articles 里没有 user_id —— 稿件是全平台共享的。这是成本能不能扛住的关键(§4)。 2. news_article_sources 是必须的,不是可选的 —— 没有它,「每条都有出处」就是一句口号。

每天那一跑

取源
RSS / 官方 API 拉取 → 落 news_items 失败:记 last_error,不中断整条流水线
去重聚类
同 external_id 直接丢;标题/正文相似度聚类 → 回填 cluster_id (为什么要聚:同一件事五家都发,不聚就会出五条一样的)
选题
每类挑 N 条(建议 N=5),按「多源覆盖 + 时效」排序 ⚠️ 只有一个源报的,降权 —— 单源孤证最容易是假新闻
AI理解
读源文本,判断这是什么新闻:类型/重要度/状态/事实层级/主角/关键数字 ⚠️ 这一步只判断不写作 —— 判断和写作混在一起,幻觉会顺着写作溜进判断
AI编辑
填出三层阅读(3秒/10秒/为什么重要)+ 第二张卡 → 一份框架 JSON(§3.0) 输入只有该 cluster 里的源文本;填不出来填 null,不许编 ⚠️ 用结构化输出(schema 约束),不是"让它自己吐 JSON 然后我们解析"
AI选视觉
在 §3.5 那 8 个母版里选一个(闭集)+ 配图;有人脸的图一律不用(§1.3) ⚠️ Jeff 点名:这一步必须和下一步分开 —— 合在一起就退化成「AI 画图」 代码当守门员:字段不齐就驳回降级,并记进 gate_report(不许静默降)
排版引擎
Renderer 按 Design JSON 精准执行:字号/字重/图 crop/留白/卡片高度/数字位置 ⚠️ 绝不 LLM → PNG(§3.5.1)
审核闸门
见 §3.6 七道 —— 不过就 blocked,当天这条不发
发布到群
status = published;按订阅扇出,写 news_deliveries(唯一键保证不重复)
群里讨论
Explain / Ask AI / Discuss / Save / Follow(§3.9)—— 这一步才是 HOOP 的

7 道闸门

任何一项不过 → blocked,当天这条不发。⚠️ 这几关必须是代码,不是提示词。

闸门 1
有出处
怎么判
news_article_sources 至少一行,且链接可达
不过怎么办
直接 blocked
闸门 2
数字锚定
怎么判
稿件里出现的数字/日期,必须能在源文本里找到
不过怎么办
blocked
闸门 3
专名锚定
怎么判
人名/机构名必须在源文本里出现过
不过怎么办
blocked
闸门 4
无人脸配图
怎么判
配图过人脸检测;有脸 → 换图或不配图
不过怎么办
去图,不拦稿
闸门 5
长度
怎么判
超过阈值 = 有整篇转载嫌疑(§1.4)
不过怎么办
blocked
闸门 6
单源孤证
怎么判
只有一个源,且属敏感类(犯罪/死亡/指控)
不过怎么办
blocked
闸门 7
事实层级不许升级
怎么判
fact_level 与源一致;源写「洽谈/据报道」,稿子里不许出现「已收购/达成」
不过怎么办
blocked

3.6 审核闸门(这是整套里最重要的一段)

每一条稿件在发布前,必须逐项过下面这几关。任何一项不过 → blocked,当天这条不发。

⚠️ 闸门 7 是 Jeff 2026-08-13 点名要的,他的原例:Reuters 报「Anthropic 正在洽谈收购 Decart AI」→ AI 不许把标题写成「Anthropic 60 亿美元收购 Decart」,交易还没完成。 怎么做成代码而不是提示词:

fact_level 五级(从弱到强):
  rumor(传闻) < reported_talks(据报道洽谈) < confirmed_talks(双方证实在谈)
              < agreed(已达成协议) < completed(已完成)

判法:① AI 填的 fact_level,必须能在源文本里找到对应措辞(in talks / reportedly / agreed…)
     ② 三层阅读 + headline 里,不许出现比 fact_level 更强的动词
        —— 维护一张「更强动词」词表(收购/完成/达成 vs 洽谈/据报道),命中就 blocked
     ③ status = developing 时,版面上**必须**显示「Developing」角标

⚠️ 闸门 2 和 3 是「防 Apple 那一类翻车」的核心 —— Apple 那几条编造的共同点就是 摘要里出现了源文本中没有的事实(自杀、夺冠、出柜、被捕)。

⚠️ 这几关必须是代码,不是提示词。 「求模型别编」不是闸门;在源文本里 grep 得到才算数

📌 Jeff 那一刀让闸门 2 从"能做"变成"好做":数字不再是从一坨正文里正则捞出来的, 而是 key_numbers[] 里一个一个躺着的,每个还自带 source_ref 指到哪条源。 所以闸门 2 变成:遍历 key_numbers,拿 valuesources[source_ref] 的原文里找, 找不到就 blocked —— 精确到"是第几个数字错了",不是"这篇有问题"。 闸门 3 同理走 entities[]这是内容与排版分家顺带买一送一的好处。

3.7 人在哪一环 —— 全自动 ≠ 无人把关

⚠️ Jeff 08-13 定的分工:不是「10,000 Spider → 100 记者」,而是 10,000 Spider → AI Research Agents → AI Journalist → AI Editor → 少量 Human Editor

人只处理高风险那一类,清单他给了,我原样收下并做成字段:

必须过人 为什么
重大政治 / 战争 错一条就是立场问题,不是事实问题
法律 / 司法 「被起诉」和「被判有罪」差一个字差一条命
死亡 / 伤亡 不可撤回;NewsBreak 那条假枪击(§1.7)就在这一类
重大企业指控 直接影响股价和名誉,律师函最快的一类
争议事实(来源打架) conflicts 非空(§3.11)自动进人工队列
Breaking(首次报道) 越快越容易错

可以全自动的:科技发布、公司业绩、体育结果、产品发布、研究论文、企业公告。 共同点:有一手件可对(财报、赛果、论文、公告),而且错了能改、改了没人受伤

判据:这条错了,谁会受到不可逆的伤害? 有 → 过人;没有 → 自动。 落地:框架里加 risk_tier: auto | human_review,由类别 + conflicts + fact_level 推出来,不是 AI 自己说了算

Jeff 要的是「全自动化」。全自动可以,但必须答出这三个问题:

问题 答案
谁能停? 每类都有 enabled 开关;一键关掉某类当天推送
出事怎么撤? status = 'retracted' + retracted_at + 原因;已推送的要能撤
多久能撤? 目标:发现到下架 < 1 小时(见 §2.2)

⚠️ 撤稿这条路必须在 P0 就通 —— Apple 的教训是:出事时你需要的是一个开关,不是一次发版。

3.8 失败与降级 —— 宁可不发,不发旧的

挂了什么 怎么办
某个源挂了 跳过它,用其余的源照常出;记 last_error
一整类的源全挂 这一类今天不发。 不发比发错好
模型挂了 / 超时 重试 2 次(退避)→ 仍失败则该类不发
闸门不过 该条不发;若整类都不过 → 该类不发
发旧的? 绝不。 新闻发旧的比不发更糟 —— 用户会以为那是今天的事

判据一句话:这套东西「安静地没发」是可接受的,「发错」不可接受。

3.9 群里的动作条 —— 新闻 → 群 → AI → 讨论(Jeff:这比排得漂亮重要得多)

Jeff 原话:「用户下面不是只有 👍❤️😂,而可以有 Explain / Ask AI / Discuss / Save / Follow。 用户点 Ask AI 就可以直接问『这会不会威胁 Nvidia?』然后在这条新闻下面直接和 AI 聊。 这就非常 HOOP 了 —— 不是『新闻 app → 看新闻』,而是『新闻 → 群 → AI → 讨论』。 这个我觉得比单纯把新闻排得漂亮重要得多。」

我同意,而且这条是护城河本身:排版谁都能抄,「新闻长在一个有群的 IM 里」抄不走(§2.1)。

⚠️ 第一版只上三个:Ask AI / Discuss / Save(Jeff 08-13 砍的)—— Explain 并进 Ask AI(它本来就是「问一个更浅的解释」),Follow 放详情页。 他的理由,我完全同意:「一张小新闻卡下面又五个按钮,就会出现你之前一直很讨厌的那个问题: 东西太多、很乱。

动作 干什么 落在哪 第一版
Explain 就这条新闻,给一段更浅的解释 并进 Ask AI,不单独出按钮 ❌ 并入
Ask AI 在这条新闻下面直接问 起一条带上下文的 AI 回复,上下文 = 这条 doc + 源文本
Discuss 把这条甩进群,开一条讨论 复用已有的转发 + 群消息
Save 收藏 一张 news_saves
Follow 追这条线后续 cluster_id 走,有后续就推 ❌ 放详情页

⚠️ 三条设计纪律(不写清楚会出事):

  1. Ask AI 的上下文只有「这条 doc + 它的源文本」 —— 不许它带着整个互联网的记忆来答。 否则用户问「这会不会威胁 Nvidia」,它会拿训练里的旧事实回答,而那些没过我们的闸门
  2. AI 的回答必须视觉上和新闻正文区分开,并标「这是 AI 的看法,不是报道」。 Apple 那次翻车(§1.2)就是用户分不清哪句是新闻、哪句是 AI 生成的
  3. 群里问的,群里能看见 —— Ask AI 的问答落在那条新闻下面,不是私聊; 否则同一个问题群里五个人各问一遍,五份成本、五个答案。

4. 成本(按现价算,标了查询日期)

🚨 规模一变,这一节就不成立了 —— 必须说在前面。 下面那些数字是 P0 假设:4-6 类、每天各跑一次、只处理选出来的那几条。 而 §3.-1 那套 Discovery Network 的量级是 Jeff 说的「一天 100 万个 URL、聚类成 3 万个事件」—— 那不是同一个数量级的东西,新增的大头是: ① 每个事件 5 个 Agent 并行找证(§3.11)→ 光调用次数就 ×5; ② 抓取带宽 + 原文快照存储(要留证,不能只存链接); ③ 聚类/向量检索;④ 7×24 的监控和重试。 我没有现在就编一个数字(编了就是假的)—— 要算得先定「一天真的处理多少条」, 那是 §6 待拍板的事。先把 P0 的 $12 和终局的成本分开写,别让人拿 P0 的数字去估终局。

⚠️ 口径(Jeff 08-13 纠正,很重要):这一节算的只是 AI text processing ≈ $12/month at current P0 assumptions,不是「整个产品每月成本 $12」。 没算进去的:配图、新闻源授权、抓取基础设施、存储、向量/聚类、监控、推送、翻译。 他的原话:「不然以后很容易误导团队。」

价格查询日期 2026-08-13,来源 = claude-api skill 的模型价目表(缓存日期 2026-06-24):

模型 输入 $/1M tok 输出 $/1M tok
Claude Opus 5 (claude-opus-5) $5.00 $25.00
Claude Sonnet 5 (claude-sonnet-5) $3.00(introductory $2.00,到 2026-08-31) $15.00(intro $10.00)
Claude Haiku 4.5 (claude-haiku-4-5) $1.00 $5.00

假设(明写,方便你改): - 4 个类别,每类每天跑 1 次 - 每跑一次:输入 ≈ 20,000 tok(源摘要 + 提示词),输出 ≈ 6,000 tok(5 条 × 约 1,200 tok)

每月成本(30 天):

模型 每类每天 4 类每天 每月
Haiku 4.5 $0.020 + $0.030 = $0.05 $0.20 ≈ $6
Sonnet 5(intro 价) $0.040 + $0.060 = $0.10 $0.40 ≈ $12
Sonnet 5(标准价) $0.060 + $0.090 = $0.15 $0.60 ≈ $18
Opus 5 $0.100 + $0.150 = $0.25 $1.00 ≈ $30

⚠️ 三个必须看懂的点:

  1. 文本成本低到可以忽略。 就算全用 Opus 5,一个月也就 30 美金上下。 真正的成本变量是「类别数 × 每天跑几次」,不是模型档次。 加到 10 类、一天跑 2 次 → 直接 ×5。
  2. 成本和用户数无关 —— 因为稿件是共享的(§3.3)。 ⚠️ 如果哪天有人提议「给每个用户单独生成」,那一刻成本就从「和类别数成正比」变成「和用户数成正比」。这是这个产品最经典的死法。
  3. 配图成本没算进去 —— 我没现查我们那条生图管线的单价。⚠️ 待补。 若一天 20 张图,这一项很可能比文字贵

建议:P0 用 Sonnet 5 起步(质量够、便宜),把 Opus 5 留给「财经/黄金」这类出错代价高的类别。 ⚠️ 这条是建议,待 Jeff 拍板。


5. 分阶段

P0-A —— Renderer Prototype(先做这个,Jeff 08-13 定的顺序)

Jeff 原话:「现在最应该做的,不是继续研究新闻源,也不是先把 AI 接完,而是马上把 Renderer 做出来。 如果这一层没过,后面数据库、AI、闸门全部做完也只是一个技术正确但视觉不成立的产品。」

  • 五种母版(Breaking / Big Story / Normal / Data / Explainer)手写假数据渲染出来
  • ⚠️ 要配图 —— 用固定的内部测试图。Jeff 纠正过我原来那句「P0 不配图」: 「没有图,你无法判断这个产品是不是漂亮。」内部测试图完全不碰版权,因为不发布。
  • 按真实聊天宽度渲:285 / 393 / 440 pt 三档都要看(§3.5 那张表)
  • 不碰:新闻源、AI、数据库、闸门 —— 一行都不写
  • 验收:拿 5 条真实新闻分别塞进五种母版,在手机上真的漂亮。 不漂亮就重来,不往下走。

⚠️ Jeff 2026-08-14 修正了他自己昨天那一刀:「Renderer 仍然应该第一优先, 但 Renderer 和 Discovery Prototype 可以平行。」两队互不阻塞:

Team A(视觉) Team B(发现)
五种母版 → 真实手机尺寸 → 做到漂亮 BNM · Bursa · SGX · MAS · 10 家公司 IR
ScoutBot / MonitorBot / FetchBot
→ Event → Evidence Pack

两队都先不做 AI Journalist。 B 队只要回答一个问题: 「机器能不能自己发现一件真实新闻,并且建立起 Evidence Pack?」

Jeff 的话我原样留着:「左边证明我们可以看到世界,右边证明我们可以漂亮地告诉世界。 中间的 AI Journalist 反而是最容易补上的。」

P0-B —— News Pipeline(Renderer 过了才开工)

  • 第 0 件事:把 §3.0 那份框架落成真 schema —— JSON Schema 一份 + Go struct 一份,两边由同一份生成,不许手抄两份(铁律 23)。 模板挑选那个规则函数先写测试:给一份 doc,断言它挑中哪个模板;字段全缺时必须落到 normal
  • 新闻源 → 框架 JSON → 闸门 → Renderer,串起来
  • 类别每天 1 次、只用 RSS 源(几类等 §6 第 9 条拍板)
  • 表:news_sources / news_categories / news_items / news_articles / news_article_sources
  • 闸门:1(有出处)+ 2(数字锚定)+ 5(长度)+ 7(事实层级不许升级) 四关先上 ⚠️ 闸门 7 进 P0 不进 P1 —— Jeff 那句「新闻不失真才是第一重要」说的就是它
  • 三层阅读要全;第二张卡不进 P0(它更容易编)
  • 撤稿开关必须通
  • 验收:同一条新闻,源里能查到出处;闸门故意喂一条编的进去,必须红; 再喂一条「源写洽谈、稿子写已收购」的,闸门 7 必须红

P1 —— 订阅与推送

  • news_subscriptions / news_deliveries
  • App 里的订阅界面 + 每天推送
  • 闸门补齐 3(专名)、4(无人脸)、6(单源孤证)
  • 群里的动作条(§3.9):先上 Discuss + Save(地基已有),Ask AI 也放这一档 —— Jeff 说它比排得漂亮重要得多,不该等到 P2
  • 第二张卡(why_talking / what_it_means)—— bull_vs_bear 留到有财经类再说
  • 验收:同一条稿件推给 100 个订阅者,news_deliveries 不能有重复行; Ask AI 的回答在视觉上和报道正文分得开,并标了「这是 AI 的看法」

P2 —— 配图与社交

  • 配图(抽象图为主)+ 人脸闸门(闸门 4)
  • 「甩进群 + 一键开投票」 ←—— 这是噱头本体,别拖到最后
  • 指标看板(§2.2 那张表)

⚠️ 我个人建议把 P2 的「甩进群」提前到 P1 —— 那才是我们和今日头条的差别; 纯看新闻的部分,我们本来就不占优。这条要 Jeff 拍板。


8. 🛠️ 技术实现 —— 开工前先理一遍(Jeff 2026-08-14 语音要的)

Jeff:「再往深处写 —— 技术、代码。bot 的 Python 怎么写、生成器怎么写、 每一张新闻卡怎么渲染,全部写进去……开工之前至少全部理过一遍。

8.0 ⚠️ 这一章的两条规矩(先说,不然它会变成第二个真相)

代码写进文档,比文字腐得快。 真代码一开始写,文档里这份就成了第二个出处 —— 一改就腐(铁律 23)。 所以:

  1. 只写「决定」,不写「填空」。 这里只放改起来要重来的部分:数据契约、全局限速、 闸门判法、模板挑选。CRUD、路由、错误处理这些照着写就行的,不进图纸。
  2. 已经有真代码的,只放指针,不复制。 例如 Renderer —— 真代码在 docs/news_cards_demo.html(线上 /cards),这里只写「它在哪、关键函数是哪几个」。 P0-B 开工后,下面这些代码块要逐段换成 file:line 指针。

判据:这一章的每段代码,都要能回答「它替我省掉哪一次返工?」 答不上来的,是装饰。

8.1 数据契约 —— 一份 schema,两边生成,绝不手抄

这是全章最要紧的一块:框架 JSON(§3.0)同时要被 Go 后端渲染器AI 结构化输出三处用。 手抄三份 = 一改就腐三次。

schema/news_doc.schema.json      ← 唯一真相(JSON Schema)
        │
        ├─ go generate → backend/internal/domain/news/doc_gen.go   (Go struct)
        ├─ 直接喂给模型 → AI 结构化输出的约束(不是提示词里描述一遍)
        └─ 渲染器读它做字段校验

⚠️ AI 那一路是重点:schema 要直接当成结构化输出的约束交给模型, 不是在 prompt 里用自然语言把字段再描述一遍。 描述一遍 = 又一份会腐的抄本。

# 生成:一份 schema → Go struct + 模型约束(示意,决定在这儿,不在具体库)
SCHEMA = json.load(open("schema/news_doc.schema.json"))

resp = client.messages.create(
    model="claude-sonnet-5",
    messages=[{"role": "user", "content": prompt_with_source_text}],
    output_config={"format": {"type": "json_schema", "schema": SCHEMA}},   # ← 约束在这儿
)
doc = json.loads(resp.content[0].text)     # 已经符合 schema,不用再解析容错

返工点: 如果这一步用「让它自己吐 JSON 再解析」,上线三个月后你会有一个专门修 JSON 的函数, 而且每次改字段要改四个地方。

8.2 Crawl Scheduler —— 全局限速那一条(§3.15.3 说的坑)

四种 bot 共用一个限速器。限速的键是 domain,不是 bot,也不是 URL。

class DomainGate:
    """每个 domain 一把闸:令牌桶 + 并发上限 + robots 缓存 + 错误预算。
    ⚠️ 全局唯一 —— DiscoveryBot / FetchBot / SourceHuntBot / MonitorBot 都从这里要许可。
       各自守规矩、加起来打挂人家,是这套系统最容易犯的错。"""

    def __init__(self, domain, rps=0.2, concurrency=2, error_budget=20):
        self.domain = domain
        self.rps = rps                      # 默认 5 秒一个请求,宁慢不快
        self.sem = asyncio.Semaphore(concurrency)
        self.tokens, self.last = 0.0, time.monotonic()
        self.errors = 0
        self.error_budget = error_budget
        self.paused_until = 0.0
        self.robots = None                  # (rules, fetched_at)

    async def acquire(self, path, bot_name):
        if time.monotonic() < self.paused_until:
            raise DomainPaused(self.domain)          # 错误预算爆了,整个 domain 停
        rules = await self.get_robots()              # 缓存,不是每篇都抓
        if not rules.allowed(path, user_agent="HOOPBot"):
            raise RobotsDisallowed(self.domain, path)  # ⚠️ 硬约束(§3.15.2)
        await self.sem.acquire()
        await self._wait_for_token()                 # 令牌桶

    def on_error(self, status):
        # ⚠️ 402 不是错误,是「要付费」—— 永久状态,不重试(§3.10 / §3.15.2)
        if status == 402:
            registry.mark(self.domain, "needs_licence"); self.pause(forever=True); return
        self.errors += 1
        if self.errors > self.error_budget:          # 超预算要有动作,不是记日志
            self.pause(hours=6); alert(f"{self.domain} 错误预算爆了,已暂停")

返工点三个,每个都真会发生: ① 限速做成 per-bot → 上线后被媒体投诉,那时改架构; ② robots 每篇都抓 → 自己把自己限死,还给对方添一倍流量; ③ 402 当成临时错误重试 → 对着一个明说要收费的站点狂敲门

8.3 闸门 7:事实层级不许升级(§3.6)

这道闸门是「代码不是提示词」的样板 —— 它是一张词表 + 一次比较,不是求模型自觉。

# 从弱到强;下标就是强度
FACT_LEVELS = ["rumor", "reported_talks", "confirmed_talks", "agreed", "completed"]

# 每一级「只有到了这一级才允许出现」的动词(中英各一份)
STRONGER_VERBS = {
    "completed": ["收购了", "已完成", "完成收购", "acquired", "completed", "closed the deal"],
    "agreed":    ["达成协议", "签署", "agreed to acquire", "signed"],
    "confirmed_talks": ["证实正在谈", "confirmed talks"],
}

def gate_fact_level(doc, source_text):
    """稿子里不许出现比 doc.fact_level 更强的动词。命中 = blocked。"""
    level = FACT_LEVELS.index(doc["fact_level"])
    # 1) AI 填的层级,必须在源文本里找得到对应措辞
    if not _level_supported_by_source(doc["fact_level"], source_text):
        return Blocked("fact_level 在源文本里找不到依据")
    # 2) 三层阅读 + 标题里,不许出现更强的动词
    text = " ".join([doc["layer1_3s"], doc.get("subheadline") or "",
                     doc["layer2_10s"], doc.get("layer3_why") or ""])
    for lv, verbs in STRONGER_VERBS.items():
        if FACT_LEVELS.index(lv) > level:
            for v in verbs:
                if v in text:
                    return Blocked(f"事实层级是 {doc['fact_level']},但文里出现了「{v}」")
    # 3) developing 必须显示角标 —— 渲染层的事,这里只断言字段在
    if doc["status"] == "developing" and not doc.get("show_developing_badge", True):
        return Blocked("developing 必须显示角标")
    return Pass()

⚠️ 词表一定要能被非工程的人改 —— 它是编辑规则不是代码逻辑,放配置里、有人审。

8.4 claim → 正文:AI 以 claim 为单位输出,Renderer 拼句子(§3.13.1)

这一步定错了就补不回来 —— 事后做「句子 → 来源」的映射是猜的。

def assemble_paragraph(block, claims_by_id):
    """正文块只引用 claim,不自己写字。
    ⚠️ 没有 claim_ids 的正文块 = 非法,直接拒绝(§3.13.1)。"""
    if not block.get("claim_ids"):
        raise InvalidDoc("body_blocks 里出现了没有 claim_ids 的正文")
    out = []
    for cid in block["claim_ids"]:
        c = claims_by_id[cid]
        if c["attribution"] == "claimed_by":
            # ⚠️ 一手件 ≠ 事实(§3.13.2):必须带主语,不许省
            out.append(f'{c["speaker"]}表示,{c["text"]}')
        else:
            out.append(c["text"])
    return "".join(out)

返工点: 先让 AI 写成段落、再回头挂来源 —— 那时候你只能猜哪句对应哪个证据, 而这正是「拿得出证据」和「AI 回忆」的分界线。

8.5 Renderer —— 真代码已经在跑,这里只给指针

不复制一份。 真文件:docs/news_cards_demo.html(线上 https://hoop-docs.pages.dev/cards)。

要看什么 在哪
Design JSON 长什么样 文件里 const DOCS = [...]
模板怎么挑 variant pickVariant(d) —— Renderer 按标题长度/关键数字个数决定,不归 AI(§3.0)
每种母版怎么渲染 renderCard(d) 里按 news_type 分支
AI CONTEXT 那一块 whyEl(d) —— layer3_why 强制带 AI 解读标(§3.13.2)
动作条 常量 ACTS —— 只有 Ask AI / Discuss / Save(§3.9)

⚠️ P0-A 的 Renderer 是 HTML/CSS;进 App 时它要变成 Flutter widget。 那时候「一份 Design JSON、两套渲染器」就是一个新的腐化点 —— 要么 App 里直接用 WebView 渲这份 HTML(小程序本来就是 WebView), 要么两边共用同一份模板描述。这一条要在 P0-B 之前定,§6 已加为第 22 条。

要你拍板的 25 条

第 1 条
法律:上线前要不要找本地律师看一次「摘要+链接」的边界
我的建议
为什么要你定
花钱的事;而且这是唯一可能整个做不成的风险
第 2 条
配图:是否接受「只用抽象图,永不用像新闻现场的图」
我的建议
接受
为什么要你定
会让版面难看一些,是产品取舍
第 3 条
类别数:P0 就 4 类,还是一上来铺满
我的建议
4 类
为什么要你定
类别数直接乘成本和出错面
第 4 条
跑几次:财经/黄金要不要加一次收盘后
我的建议
P0 先不加
为什么要你定
成本翻倍
第 5 条
模型档:Sonnet 5 起步 vs 全 Opus 5
我的建议
Sonnet 5 起步
为什么要你定
每月差 ~$18,不是大钱,是质量取舍
第 6 条
「甩进群」提前到 P1?
我的建议
提前
为什么要你定
那是噱头本体
第 7 条
通讯社授权:要不要去谈 AP/Reuters
我的建议
先不谈,RSS 起步
为什么要你定
要花钱、要谈判,量级我查不到
第 8 条
框架 v1 字段够不够(§3.0)—— 你点的那几样我都放进去了,另外补了 news_typestatusbody_blocksentitiesschema_version
我的建议
v1 就到这里,先跑起来再加
为什么要你定
字段发出去就有旧稿在用,加容易删难
第 9 条
分类到底几类:你 08-13 那段给的是 World / Business / AI / Sport / Entertainment 五类,更早语音点的是财经 / 黄金 / 娱乐
我的建议
五类 + 加一类 🇲🇾 Local(马新),黄金并进 Business
为什么要你定
前五类大平台都有且比我们全,本地是唯一我们能赢的一块(§3.1)
第 10 条
名字 HOOP NOW 定了吗
我的建议
为什么要你定
定了我才好在别的文档里统一叫它
第 11 条
P0 要不要先只做 Renderer(拿手写假数据把五种母版渲出来给你看,你点头再接 AI)
我的建议
为什么要你定
不然「版式不好看」和「AI 写得不好」会搅成一个问题,谁也说不清
第 12 条
importance 用 1-5 整数还是 8.7/10 一位小数
我的建议
1-5 整数
为什么要你定
一位小数是假精度,而你会拿它排序(§3.0)
第 13 条
母版到底六个还是八个 —— 图上八个,后来那条说六个(少了 Timeline / Multi-news)
我的建议
按八个写、MVP 先做五个
为什么要你定
留着不吃亏,不做就是了
第 14 条
Spider 什么时候开工 —— 它是终局的地基,但也是唯一有法律风险的一步
我的建议
P0-B 只用 RSS/Sitemap/官方公告;Spider 放 P1,而且上线前要有 robots 白名单和限速
为什么要你定
抓取是唯一可能被人告的一步(§1.7 那 175 万)
第 15 条
一天到底处理多少条(1 万?10 万?100 万 URL?)
我的建议
P0 每类每天 5 条起步
为什么要你定
这个数字直接决定成本量级,不定它我算不出终局成本(§4)
第 16 条
Event Graph 现在做多少
我的建议
只做「事件为单位 + 每条 claim 挂证据 + 原文快照」这三样,其余等
为什么要你定
这三样事后补不回来(源页面会改会删);实体图谱以后做也不迟(§3.12)
第 17 条
GREEN → YELLOW 的出闸条件里 N 和 M 定多少(连续多少条零纠错 + 抽检多少条)
我的建议
N=200、M=50,按类别各算各的
为什么要你定
用可测的代替「跑三个月」(§3.13.3)
第 18 条
体育结果要不要进第一批 GREEN —— 赛果数据常常是有主的
我的建议
第一批不进,等确认数据来源授权
为什么要你定
你写的「权利明确的前提下」得先有答案(§3.13.3)
第 19 条
原文快照留多久 —— 留证要存原文,但存原文本身是复制
我的建议
只内部可查、不对外,保留 24 个月
为什么要你定
留证和转载是两件事,但边界要自己划清楚
第 20 条
opt-out 做成什么 —— 公开 bot page 就一定会有人 opt-out
我的建议
自助表单 + 当天生效 + 一张公开名单,不是一个邮箱
为什么要你定
只留邮箱 = 姿态;判据同 robots:不用找律师就能拒绝我们
第 21 条
/bot 那一页挂在哪个域名
我的建议
hoopcomm.com/bot
为什么要你定
要公开 IP 段和联系方式,得先定域名
第 ~~22~~ 条
~~App 里的渲染器怎么办~~ —— 2026-08-14 有答案了,不用再拍:平台本身就是小程序(WebView),所以渲染器就是这份 HTML,不做第二套 Flutter 实现
我的建议
✅ 已定
为什么要你定
一份 Design JSON 只有一个实现,不会腐
第 23 条
Live 管道的门槛用「有没有一手件」还是「importance=5」
我的建议
用一手件(§3.21)
为什么要你定
importance 是 AI 判的,而 Breaking 恰恰最容易错;一手件是客观的
第 24 条
Live 每小时最多推几条
我的建议
同一类 每小时 1 条,全类合计每天 ≤ 5
为什么要你定
不设上限就是你说的「新闻轰炸」
第 25 条
Reputation 冷启动:多少条之后实测分才盖过先验
我的建议
每个领域 100 条已判定事件
为什么要你定
不定这条,新源要么进不来要么一进来就是 A(§3.19)

6. ⚠️ 要 Jeff 拍板的(我没有替你决定)


7. 这份图纸没做的事(边界)

  • 小程序平台的任何设计 —— 归 Fable(Jeff 2026-08-13 明确)
  • ❌ 任何代码、迁移文件、迁移号
  • ❌ 部署与发版

写于 2026-08-13 · Vega(平台架构/接缝线) 下一步:等 Jeff 看完 §6 那几条,拍完板再谈动手。