📰 HOOP NOW · 新闻小程序 · 架构图纸(调研 + 设计)
🕒 这一版生成于 2026-09-30 14:25 · 代码 a0316a2f1
唯一网址 → hoop-docs.pages.dev/news
图纸 · 未开工 2026-08-13 这一页由 docs/NEWS_PLATFORM.md 生成 —— 改内容改那份 md,别改这个 HTML。小程序平台本身归 Fable,这份不碰。

0.0 ⛓️ 三条铁律 —— Jeff 2026-08-31 亲自定的,绝不违背

他原话:「把这三条铁律写下来!我们绝不违背 也绝对这样执行!」 这一节放在最前面,因为它管着后面每一行代码怎么写。改这三条要他本人开口。

第一道 · 发现 —— 我们只是去看

追求新闻真相,任何人、任何告示都不该让我们停下脚步。 robots.txt 那种告示是礼貌,不是法律;我们的抓取只是求知欲。公开看得到的东西,我们去读。 人家要封我们,那是他的权力 —— 但他不能阻止我们。

这一道唯一的边界(线过去了性质就变): · 不用别人的账号,不破解,不钻漏洞白看。 · 需要登录/付费才看得到的,我们正正当当去注册、去订阅、去付费,用自己的账号读 —— 我们就是合法读者,那不叫绕过,那叫花钱看。 · 这一条不是礼貌问题,是法律:马来西亚《计算机犯罪法 1997》第 3 条、新加坡《计算机滥用法》 管的都是「未经授权取用」。门开着走进去,和门锁着撬开,是两件事。

⚠️ 现实代价(不是道德问题,是生意):不理告示的代价通常不是被告,是被封 IP —— 那个源就永远失去了。该守的守、该谈授权的去谈,别为了省一步把最有价值的路堵死。

第二道 · 编写 —— 我们自己写,自己负责

尊重版权。绝不搬抄任何人的内容上架。自己写、自己交叉比对、对自己的内容负责。

⚠️「让 AI 把人家的报道改写一遍」不算自己写。 换了词,它仍然是从人家那篇长出来的。 真正的自己写 = 从事件出发,回到一手件(部长的原文告示、交易所公告、统计局数字)自己组织。

判据(这句已经写在 tools/news_source_atlas.yaml 里,是同一条):

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

⚠️ 这一条必须落成代码,不能只写在提示词里 —— 提示词挡不住,代码才挡得住。

第三道 · 分工 —— 两道之间不许混

第一道只负责「知道发生了这件事」,第二道才负责「写出来」。 第一道读到的东西不是稿子的原料,是线索。

为什么这条最要紧: 外面那些 AI 公司现在挨的官司,几乎全是版权官司,没有一宗是因为不理 robots。 也就是说 —— 第一道怎么读都不是要害,要害在第二道有没有搬抄。


一眼看完

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,见下 (⚠️ 09-09 注:只守 Crawl-delay,Disallow 已被 §0.0 作废)
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、限速。

    ⚠️ 这一条的「有名有姓」「遵守 disallow」两半已被 §0.0(08-31,Jeff 亲定「robots 是礼貌不是法律」)和 09-01 的身份决定(spider_engine.py IDENTITY 默认 browser,「代表真人去浏览」)作废 —— 现在只守限速(Crawl-delay 自动读、只放宽不收紧)。留在这里是为了让后来人知道曾经这么定过、以及为什么改;别照这一行写代码。 判据:一家媒体想拒绝我们,要有一个不用找律师就能做到的办法。

  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 刚让我把 importance 从 8.7/10 改成 1-5,理由是模型给不出稳定的 8.6 vs 8.7。 0.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_class 和 trust_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_policy 和 use_for_content 是每次发布都要查的闸门条件,得是能加索引、能写约束的列。

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

⚠️ 09-09 注:本节及 §3.10 / §8.2 提到的「402 = 永久进要付费名单」还没落到引擎里(引擎对 402 按普通 4xx 处理:一次不重试、不搁置出口);要做时在 spider_engine.note_outcome 加一条并配守卫。

⚠️ 2026-09-09 注:这一节的「HOOPBot/1.0 身份 + 守 Disallow」两点已被 §0.0 和 09-01 的身份决定推翻(默认 browser 身份、只守 Crawl-delay;引擎里 disallow 这个词连出现都有守卫禁着)。§3.15.2 的 robots 缓存、§3.15.3 的按域限速 / 错误预算这些仍然有效并已落地(robots_delay 30 天缓存、每主机每班 60 次、429 按 Retry-After 整源暂停)。读到和 §0.0 打架的句子,以 §0.0 为准。

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 —— 指到条款那一句。没有链接的 ✅ 不许存在。

🚨 同一份数据有两条出口,我只堵了一条(2026-09-13,Gode 复核 + 实测)

上午我把待审稿从 ourArticles 里滤掉,报告说「没审过的稿不对外」。那句话当时是假的, 因为同一份数据还有两条出口我没看:

出口二:那把小程序共享钥匙是「全员读」。 读它的那条路只看小程序是不是 active, 不看你是不是房里人。实测:造一个不在房里的真账号去读,拿到 30 篇稿、其中 9 篇待审带全文,http 200。 所以「待审稿只在 HOOP 里看得见」从来没成立过 —— 任何登录用户都读得到。 ⚠️ 判据:一条全员读的通道上,没有任何「只给自己人看」的东西是安全的。 能加的标记只决定「谁来读这一份」,决定不了「谁能拿到这一份」。 修法:待审稿改走 GET /v1/news/drafts(要票 + 要是这间房现在的成员); 共享钥匙里只留所有人都可以看的。

出口三:总览里那块「表导出」按 news_ 前缀自动全表导出。(Gode 点名要堵) 于是 news_articles 整张被倒出去 —— 带 doc 全文、带 draft,而且是不带票就拿得到。 news_article_versions 也有 doc,同样漏;当天新加的 news_article_reviews(签字历史) 按那个规则明天就会自动上公网。 修法:改成明确的允许清单(publicTables 表白名单 + 列白名单 + publicTableWhere 行过滤), news_articles 只给九列、且只给已发布的行。 ⚠️ 判据:清单要写「允许什么」,不能写「排除什么」。 按前缀自动导出 = 以后任何人建一张 news_ 开头的表,它第二天就自动上了公网,而没有任何一处会提醒他。 白名单的代价是「加表要回来加一行」,而那一行正是我们要的那次停顿。

🩸 这三条的共同形状,比每一条本身重要:同一份数据有几条出口,我只数了一条。 判据:关一扇门之前,先问「这份数据一共有几个出口」,并且把它们一个个列出来, 而不是关掉眼前这一扇就报告「关好了」。

🔒 may_use_images 现在真的拦得住了(2026-09-13 补)

在这之前它谁也不拦。 它只出现在建它那条迁移、一句注释、登记簿、一条「这列还在吗」的结构测试、 源登记表工具和种子脚本 —— 整条链路上没有一处在输出图片地址之前读过它, 而 Discover 那一页显示的正是这些源的图。26 家全是 false,页面照显示了几个月。

判据(这件事真正的教训):一个没接上的策略字段,比没有这个字段更坏 —— 它让我们以为自己有一道闸。9 月 13 日三个人围着它讨论了十几分钟, 才发现它从第一天起没拦过任何东西。这和「只打印不拦截 = 白做」是同一条, 只是这次那个「只打印」的东西长得像一张策略表。

现在怎么拦(Gode 2026-09-13 判的「甲」+ 他给的验收口径):

· 后端在输出地址之前拦,不是前端把 img 藏起来 —— 他的原话:「只在页面上藏 img 不够」。那条总览接口是公开的、不带身份的, 藏起来的地址照样躺在载荷里,谁打开都看得到。 · false → image 给 null,原始地址照旧留在库里(证据不删,只是不往外送)。 · 「有图但不许给」和「本来就没抓到图」分成两件事,不共用一个 null: 多一格 image_held,"no_image_licence" = 前者,"" = 后者。 ⚠️ 给的是机器码不是人话 —— 人话要进页面那本字典,写库端和显示端两处措辞一改就腐(铁律 23)。 · 授权撤回之后旧的也不该再显示:总览那把钥匙每分钟由后端重写一次, 所以改完一分钟内全网跟上,不用等发版。

守卫:backend/internal/domain/news/image_licence_db_test.go,真库真 SQL,三种情况各钉一条。 三把刀各红:不看授权照旧吐地址 🔴1 · 有图不给却不说一声 🔴1 · 本来没图也说成「不给」🔴2。

⚠️ 2026-09-15 补:这道闸当时漏了一件 —— 它把自己人也挡在外面了。 主人打开 Discover 看不到任何图,而线索本来就是给自己记者看的、不是发布。 判据:做一道「不许对外」的闸时,要同时问「自己人从哪条路看」 —— 没有那条路,对外的闸就变成了对内的墙。补法见 §3.7.5(另开一条带身份的路,不是松这一道)。

⚠️ 这不代表那 26 家法律上都禁止(Gode 特别说明)—— false 同时装着「不允许」和「还没核过」,而现在 26 家都属于后者。 要把某一家变成 true,只有一条路:读它自己的条款,把那一句的地址记进 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_policy 从 unknown 改成别的
标记「这个源看起来值得升级」 升级它自己(要人过一眼)

它可以自动长到 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_story ↔ big_story),所以还是一套词、不用对照表; 但它们是两个字段,variant 由 Renderer 挑(按图片、人脸位置、标题长度、关键数字多少)。 判据:AI 说「这是什么新闻」,Renderer 说「这一条长什么样」。

值 什么时候用 它必须有 MVP
breaking 突发 / 重大事件 location 或 entities ≥ 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.99 和 31,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,拿 value 去 sources[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.7.1 谁看得到哪一篇 —— 两条路,两种可见范围(2026-09-13 补)

上面那张表一直是设计;代码从 000320 起到 2026-09-13 之间没有照它做。 GET /v1/news/overview 是公开的、不带身份的(那一页公开直出、带不了票), 而它把 news_articles 整张端出去、一条 status 都不筛。后果是: 一篇稿写完那一秒全世界就能读到全文,审不审都一样;retracted 也照样端出去 —— 「撤稿」那个状态当时只是一句自我安慰。

9 月 13 日凌晨那条地震稿把 USGS 的 tsunami=0 写成「没有海啸威胁」, 上线一分钟后才被复核退回。那一分钟它是公开的,署的是 HOOP 的名。

现在是两条路、两种范围,共用同一段 SQL(写两份查询迟早不一致,铁律 23):

谁在看 看得到 怎么实现
外面的人(公开 HTTP 口) 只有 published ctx 里没有内部标记 → 只放 publicStatuses
HOOP 里那一页(小程序共享钥匙) 待审的也看得见 syncOverview 把内部标记放进 ctx
两边都看不到 retracted 单独一条 WHERE,不靠「不在名单里」

三条设计判据,改这一块之前先读:

  1. 那个内部标记是进程内的私有类型,不是 header / 参数 / cookie —— 外面的请求写不进来,所以伪造不了。判据:能被外面的人写的东西不能当闸。
  2. 缺省方向是关的(fail closed):ctx 里没有标记 = 不给待审稿。 猜错时代价往哪边倒 —— 猜「不给」最多是页面少几条,猜「给」是没审的东西上了公网。
  3. retracted 单独挡,不靠名单:哪天有人往 publicStatuses 里加一个状态, 撤稿不该跟着被放出来。

守卫在 backend/internal/domain/news/news_visibility_db_test.go,真库真 SQL,不是扫源码 —— 这条规矩整个长在一句 WHERE 里,扫源码只能证明那句话还在,证明不了它真的挡住了。 四把刀各红一处:公开口不筛 status · 内部标记永远为真 · 撤稿不单独挡 · 把待审加进公开名单。

3.7.2 谁能签字 —— 三条闸,服务端拦(2026-09-13 补)

status 从 draft 走到 published,现在有三条带身份的路(backend/internal/domain/news/review.go):

路 谁可以 还要满足什么
POST /v1/news/articles/{id}/verdict admin 及以上 不能是这篇稿的作者 · 作者必须是已知的 · 结论只收 pass / fix / hold
POST /v1/news/articles/{id}/publish 只有 owner 只能从 passed 来 —— 房主也绕不开审核那一步
POST /v1/news/articles/{id}/retract 只有 owner 按第二下不报错(图纸 §3.7:出事时你要的是一个开关)

权限的出处只有一个:这间房里的 conversation_members.role。 今天 conversations.owner 和 miniapps.dev_id 恰好都是 jeffchoong —— 那是两个出处碰巧一样, 不是同一件事。哪天房里换了 owner、或者小程序换手,两个出处一分家,闸门的行为就要看代码当时问的是哪一个。 所以只认一个、写死,miniapps.dev_id 在那一份里故意一次都不查(铁律 23)。 房是哪一间也不写死会话号:从 miniapps.workroom_id 查。

三条设计判据:

  1. 「有没有资格审」和「不能自审」是两条规矩,不是一条。 只比作者,会让记者 A 审记者 B,也会让任何普通房成员写结论 —— 那是扩权,不是简化(Gode 2026-09-12)。
  2. 作者永远由服务端按当前登录身份写,绝不收请求里自报的。 能自报,「不能自审」一秒被绕过去:交稿时把作者写成别人,回头用自己的号来审。
  3. 作者不明 = 不放行。 老稿是在 author_uid 存在之前写的,判不了「审的人是不是作者」—— 判不了的时候按拦处理。补救不是在闸门里编一个作者,是让写稿入库那条路把作者记上。

守卫:review_db_test.go,真库真路由真权限(假的只有登录门)。七把刀各红在该红的地方 —— 谁都能审 🔴1 · 拿掉不能自审 🔴1 · 作者不明改成放行 🔴1 · 主编也能发布 🔴3 · 房主绕开审核直接发 🔴2 · 拿掉「不在房里」那道闸 🔴3 · 认不得的结论当成过 🔴4。

🩸 第六把刀的第一版是绿的,而且绿得有道理:局外人的 role 是空字符串, canReview("") 本来就是 false,于是被后面那道「只有主编能写结论」顺手挡住了。 403 是真的,但挡住它的不是我要钉的那道闸。撞上了不算钉住。 改法是断言错误码(not_member)而不是只看 403。 这也不只是测试的事:对房外的人回「只有主编能写结论」,是在暗示他「进了门就能审」, 而真话是他根本不在门里。

🧍 最后那一下必须是真人按的(2026-09-13 补)

Jeff 两次说的都是同一件事:「要我们这些真人再来审核」「最后有一个真人审核的比较安全」。

🩸 我原来把它做错了,而且是 Gode 当场按住的。 我两次建议 Jeff「把 Gode 设成这间房的管理员, 他就能替你审」—— Gode 的原话:「把我设成管理员不会把我变成真人。我可以做 AI 初审, 但不能把我的管理员审批记成『人工审核』。」 我把「谁有权限审」和「谁是真人」当成了一件事,而这正是他 09-12 拆过一次的那个错 (「不能自审」和「有没有资格审」是两条)的另一个位置。 一个 AI 的签字被记成人工审核,比没人审更坏 —— 记录上看着是审过的。

所以发布那一步多一条:按的人必须是真人账号(users.is_agent 为假)。 查不到就当不是真人(fail closed):猜错时代价往哪边倒 —— 猜「不是」最多多按一下, 猜「是」是让一个 AI 的签字落成人工审核。

⚠️ 只卡发布,故意不卡撤稿。 撤稿是往安全那边倒的动作;为了「必须真人」把撤稿也卡住, 等于让一篇错稿在台上多留一会儿(图纸 §3.7:出事时你要的是一个按得下去的开关)。 守卫里专门有一条钉着「AI 房主撤得掉」,防的就是这条闸被顺手扩大。

🔏 发出去的必须是被审过的那一版

Gode 同日:「改稿或换图后旧确认失效。」在这之前这条路走得通: 审过 → 换掉正文或换掉配图 → 照样发得出去,而发出去的那一版没有人看过,记录上却写着「已通过」。

做法:通过那一刻记下 md5(doc::text)(整份 doc,换图也算改 —— 图和字一样会骗人), 发布时再对一次,对不上就拒、要求重审。用 jsonb 转文本是因为 Postgres 会归一化键序 —— 我们要挡的是「内容变了」,不是「序列化顺序变了」;假红和漏报一样坏。 没有指纹(这一列存在之前审过的行)一律不给发布,不是当它通过。

🩸 改这一刀时打红了一条既有测试,而红得对:那条测试直接把稿子塞成「已通过」、跳过了审核那一步, 而真实流程里 passed 只会从 verdict 来。测试的起点,真系统走得到吗?走不到就不该拿它当起点。 改成走真路之后绿。

🪪 签字的请求必须带上「我看着的是哪一版」(2026-09-13,Gode 复核后补)

这一层比服务端自己的时序问题更外面,服务端再读一次是补不上的:

真人打开 A,后台变成 B,他再点下去 —— 服务端这时读到的是 B,于是把 B 当成了他确认的那一版。

「他看到的是哪一版」这件事只有前端知道。所以每一个签字类请求(审核 / 发布) 都要带 base_version(那一版 doc 的 md5),服务端把它钉进 WHERE 原子比对。 不带就拒 —— 缺省方向必须是拒:少带一个字段最多多按一次, 放行意味着有人替他确认了一版他没看过的稿。

发布那条 WHERE 要同时钉住三样:他看着的那一版 = 审过的那一版 = 现在库里这一版。 少钉任何一样,都能让「一版被审、另一版被发」发生。

🩸 我上一版 verdict 写的是 reviewed_doc_sha = md5(doc::text)(写入时现算)。 主编看着 A 点通过 → 缝里换成 B(状态还是 draft)→ 我把 B 的指纹记成「审过」, 给一版没人看过的稿签了字。Gode 代码复核时抓到的,我自己的并发测试没覆盖到这条路。

🪪 审 / 发的人当时是人还是 AI:存快照,不反查

Gode 的原话:「不能靠今天反查账号类型去改写过去的含义。旧记录无法确认类型就标未知,不补成人工。」

一个账号今天是 AI、明天被改成人(或者反过来),历史记录的含义不该跟着变 —— 「这一篇当时是谁签的、他是不是真人」是已经发生、不该再变的事实。 所以 reviewed_by_kind / published_by_kind 在签字那一刻由服务端从 users.is_agent 写死, 默认 '' = 不知道(这几列存在之前的行),标未知,不补成人工。 界面上要分开显示「AI 初审」和「人工审核」。

守卫里专门有一条:审完之后把那个账号的类型改掉,历史那一行不许跟着变。

🕳️ 例外要点名,不能拿「他明写了」当通行证(2026-09-13,Gode 复核后收窄)

触发器第一版的例外写的是「同一条 UPDATE 里明确改了状态的,以它写的为准」。 那个例外开得太宽:一条 UPDATE … SET doc = 新的, status = 'published' 就整个绕过去了,公开那条路当场读到一版没人审过的稿。

判据:例外要点名。「他明写了状态」不是豁免理由 —— 「他明写了」正是绕这道闸的人会做的事。 现在唯一点名放行的是 retracted (撤稿要撤得干净,不能被顶回待审)。

📜 当前有效的那一次确认,和发生过的每一次,分开存

Gode:「清空当前确认可以,但过去谁审过哪版应保留历史……当前有效确认和历史分开存。」

🩸 第一版触发器把 reviewed_by / reviewed_at / reviewed_doc_sha 清成 NULL, 而那是唯一记着「谁审过哪一版」的地方 —— 一改稿,那段事实就没了。

现在两处各干各的: · news_articles 上那几格 = 当前有效的那一次确认(会被清、会被覆盖) · news_article_reviews = 发生过的每一次,只 INSERT,代码里没有任何一处删它

连「自动作废」本身也记一行(action='revoked_by_edit')—— 「这一版为什么不算数了」和「这一版被谁批过」一样是要留的事实。 ⚠️ 那一行的 actor 故意留空而不是填改稿的人:触发器看不到「是谁在改」, 编一个进去就是伪造签字(和作者那一列不许编是同一条)。

⚠️ 审核 / 发布记不进历史就不算成功(回 500,并明说「别当成审过了」)—— 一次没有留下记录的签字和没签一样,这套东西全部的意义就是事后查得出是谁、在哪一版上、什么时候。 撤稿是例外:记不上也照样撤,日志留痕 —— 为了「历史没记上」把撤稿判成失败, 等于让一篇错稿留在台上。

四把刀各红:例外退回「明写状态就放行」🔴2 · 审核不记历史 🔴4 · 发布不记历史 🔴1 · 触发器只清不记 🔴2。

🔒 记不上历史就整个回滚 —— 回应失败 ≠ 数据库回滚

🩸 第一版我写的是「先更新稿件,再独立记历史;历史失败就回 500,并明说『别当成审过了』」。 听起来很严谨。但那时更新已经提交了 —— 稿子已经发布、已经公开,只有回应说失败。

这正是本文别处那句话:拦住了回应却没拦住写入,比两样都没拦更坏。 而且提示语越明确,那个状态越骗人 —— 它在描述一件已经发生了的事,却让人以为没发生。

现在:更新稿件和记历史在同一个事务里,记不上就整个回滚。 提示语也改成「这次审核不算数(稿子没有被改动)」。 ⚠️ 撤稿仍是例外:记不上也照样撤、日志留痕。

验收口径(Gode 点名):不许只断言 HTTP 500 —— 要查状态、当前确认那几格、以及公开那条路读不读得到,三样都没变。 留一个只在测试里非 nil 的钩子,让「记历史」这一步确定地失败。 两把刀各红:审核退回「先提交再记历史」🔴1 · 发布退回「先提交再记历史」🔴2。

🖊️ 「谁改的」由编辑接口自己记(Gode 2026-09-13 定,尚未实现)

自动作废那一行的 actor 留空是对的 —— 触发器看不到是谁在改,编一个进去就是伪造签字。 但那样那一行就没有「谁」。补法不在触发器里,而在改稿那条路上: 编辑接口自己另记一条编辑事件(它知道操作者是谁)。 ⚠️ 这一条还没做 —— 现在还没有编辑接口。等那一屏做出来时一起接,别忘了。

🩸 还有一把刀砍出的是 panic 不是「放行」:那一行比较原来解引用一个指针,安全全靠上面那个 nil 判断护着。 已改成先取本地变量再比,全程不解引用 —— 一把砍出 panic 的刀,是在告诉你这儿有个只靠邻居护着的洞。

⚠️ 真数据里还没有任何一篇稿能走完这三条路 —— 那些稿的 author_uid 是空的 (它们是在这一列存在之前写的),而「作者不明 = 不放行」。下一刀在写稿入库那一侧: 入库时把作者记上,这三条闸才有东西可拦。 (2026-09-14 更新:这句原话是「还没有任何一篇稿能走完」,已经不准 —— 路本身是通的,走不完的是那批没有作者的老稿。见下面 3.7.3。)

3.7.2b 🚨 部署顺序:页面那一笔必须先上(2026-09-14 定)

这条不是流程洁癖,是一个有窗口期的真洞。

后端这一支给 ourArticles 的载荷加了两个字段:article_id(完整 id)和 version(md5(doc::text))。 而 HOOP NOW 读稿页上那两颗签字按钮,画的条件恰恰是「载荷里有这两样」。 今天线上按不出来,不是因为拦住了,是因为 master 那版后端根本不发这两个字段 —— origin/master 里 article_id 出现 0 次(Tora 自己两边对过)。

所以:

顺序 线上会变成 结果
后端先部署 旧页面(按钮还在)+ 新后端(开始发字段) 🔴 两颗死按钮当场冒出来:画得出来、按下去什么都不发生(桥上没有发请求的能力)、人以为自己签过字了
页面先上 新页面(按钮已拆)+ 任意后端 🟢 安全,与后端版本无关

规矩:拆按钮那一笔(c1f1d4897)必须先上线,后端这一支才能部署。 · 拆按钮是安全,不依赖后端,随时能发; · 把那句话换成审稿台链接是体验,它才需要等地址,晚一步没有任何代价。 · ⚠️ 一开始两件是打成一笔的(为了「免得为一句话按两次」)—— 那等于让一件安全的事去等一件体验的事,而且等的正好是后端部署本身,顺序整个反了。 多按一次的代价,比那段窗口便宜太多。

判据(比这一次更通用):两个面各改了一半时,问「哪个先上是安全的」, 而不是「哪个先做完」。 把消除风险的那一半排在制造风险的那一半前面; 两半都在等对方,是这类事故最常见的形状。

🩸 顺带记住这一次真正的教训是 Tora 说的: 「live 72 今天是安全的,但那份安全是借 Nova 的,不是我自己的 —— 线上那版后端不发那两个字段而已。」 一件事今天没出问题,要问的是「这份安全是谁给的」; 答案是别人的话,它随时会被收回去,而且对方根本不知道自己在给。

3.7.3 在自己机器上把这三条路走一遍(2026-09-14 补)

这套闸只有在「有房、有角色、有稿」的库上才验得了,而那三样以前只长在某一台机器的手敲 SQL 里 —— 别人复现不了,只能看截图。现在是一条命令(backend/cmd/newsdev/main.go):

cd backend && make newsdev     # 跑迁移 + 建房 + 四个角色的账号 + 两篇稿,并打印起后端的命令

🖥️ 库从哪来,三级(2026-09-14 补)。 原来它只认本机 5432,而 Tora 那台 mini 上 psql / docker / brew 全都没有(Homebrew 归别的账号,他装不了)—— 所以我那句「各自能起各自的」在他机器上根本不成立。 洞还是长在交界上:我一次都没问过「你那台有什么」。 现在:

  1. POSTGRES_DSN —— 你自己指(仍然只认本机);
  2. 本机 127.0.0.1:5432 连得上 → 用它;
  3. 都没有 → 起一份内嵌 Postgres 并留着,和 internal/testdb 同一个包、 同一个二进制缓存(下过一次就不用再下)。地址 127.0.0.1:55432, 数据在 ~/.hoop/newsdev-pg(固定目录,不是临时目录 —— 不然每跑一次都是一张新库,「可重复跑」就成了空话)。 关它:make newsdev-stop(数据留着,下次再起还在)。

为什么「留着」行得通:那个库用 pg_ctl start -w 起,它是 daemon —— newsdev 种完就退出,库还活着,后端连得上。 走这条路时打印出来的启动命令会带上 POSTGRES_DSN; 照抄默认那条会连到一张没有这间房的库,而且它不会报错,只是什么都查不到。

2026-09-14 在这条路上从头走了一遍(我这台 5432 是通的,所以用 -embedded 强制): 起库 → 迁移到 393 → 种房 → 后端连上去 → 局外人 403 not_member · 主编发布 403 not_owner · 房主版本错 409 raced · 房主版本对 200 published → make newsdev-stop → 再起,签字流水那条还在(追加式的那张表,种子不动它 —— 所以它才证得了持久化, 稿子状态证不了,种子每次都会把它重置)。 三张库(我的脏库 / 一张空库 / 这份内嵌)种出来的房号、账号、稿号、版本哈希逐字一致。

· 账号 id 由名字算出来(uuid v5),每台机器算出来一模一样 —— 对起来不用互相报数字,截图里的号就是你库里的号。 · 四个角色各管一件事:owner(真人,能签也能发)· editor(admin,能签不能发)· writer(分身,稿子的作者,两样都不能)· outsider(不在这间房,验「挡在门外」)。 · 两篇稿:一篇没人碰过的 draft,一篇 passed 且 reviewed_doc_sha 就等于它此刻的版本 —— 房主按「发布」能一路走到底。种子自己会核这一条,对不上就拒绝写, 免得给出一条按下去才发现走不通的路。 · 登录走固定码 123456(users.fixed_otp;本机没配 AGENT_LOGIN_SECRET,那道门是开的)。 · 审稿台:http://127.0.0.1:8080/news/review。

⚠️ 跑测试用一张干净的库,别用你天天用的那张。 有几包的库测试(internal/mcp 至少三处) 用的是写死的用户名 / 写死的 id,自己又不清理 —— 空库第一遍绿,同一张库第二遍 23505 / name_taken。 2026-09-14 实测:同一份代码,脏库 3 条红、干净库全绿。所以:

HOOP_TEST_DSN=postgres://hoop:hoop@127.0.0.1:5432/<一张空库> go test ./internal/mcp/

(testdb 认这个环境变量,见 backend/internal/testdb/testdb.go 抬头那三级。)

🩸 这条命令一开始是假的,我写完当场被自己打脸 —— HOOP_TEST_DSN 那条路原来不跑迁移,指一张真空库上去就是满屏 relation "users" does not exist,看着像代码错了,其实还是「桌子不对」。 更难看的是:同一个洞我 09-13 已经在本机 5432 那条路上堵过一次, 教训注释就写在它下面三行,而另一条出口我一次都没数。 (判据还是那条:关一扇门之前先问「这份东西一共几个出口」。) 现在 HOOP_TEST_DSN 这条路也跑 migrate.Up,跑不动就直接报错、不悄悄退回内嵌那张 —— 是你点名要这张桌子的,悄悄换一张比报错更坏。

⚠️ 在那两条「跑第二遍会红」的测试修好之前(已单独记卡),每次拿一张新的空库; 同一张库连跑两遍,第二遍仍会红 name_taken 和一条附件 404。 判据是那条老的:「跑第二遍会怎样」要和「跑第一遍」一样被当成一个真实场景。

🔒 它只认本机库,POSTGRES_DSN 指着别处直接退出,而且没有绕过开关。 它建的是固定码账号 —— 邮箱可预测 + 码写在文档里,放到别人的库上等于四个后门。

2026-09-14 用它实测的一遍(六条,每条都落在该落的地方):

谁 做什么 结果
outsider 看待审队列 403 not_member
writer(member) 写结论 403 not_editor
editor(admin) 写结论 pass 200,status → passed
editor(admin) 发布 403 not_owner
owner 发布,但 base_version 是错的 409 raced
owner 发布,版本对 200,status → published

🩸 顺手修掉的一个今天还不会咬人、但迟早会的地方:待审队列那个循环里, 一行扫不动就 continue,一声不吭。今天那几列都是 NOT NULL 带默认、唯一可空的 扫进指针,所以扫不动是不可能的 —— 但哪天有人把某一列改成可空,队列会静静变短, 而「一篇稿从队列里消失」和「本来就没有这篇」在屏幕上长得一模一样, 那篇稿就永远等不到人审。现在掉一行 slog.Error 吼一声,响应里也带一个 dropped 数(正常永远是 0)。 判据还是那条:沉默的几种意思必须分得开。

3.7.4 📱 在 App 里直接看草稿 —— news_drafts(2026-09-14)

主人的原话:「我现在是 admin,我要进去看这个草稿的新闻,然后我不应该再登录。」 所以这条路带着他已经有的票走,页面一个字的登录都不要:

hoopKv('data','news_drafts') → 壳 → GET /v1/apps/{app}/data/news_drafts → DraftsForCaller

· 权限只有一个出处:newsroomRole(和审稿台那一页同一份 SQL、同一间房)。 这儿绝不另写一套判断 —— 两处判断迟早不一致,而不一致那天没有人会收到通知(铁律 23)。 · 负载长什么样以代码为准(backend/internal/domain/news/review.go 的 DraftsForCaller)。 这里不抄第二份:抄一份就会腐,下次加字段的人不会回来改这一段。

⚠️ 接它的人必须把这四种「什么都没有」分开,它们在屏幕上长得一模一样,意思却完全相反:

收到什么 真正的意思 页面必须说
v = null 没登录 / 不在这间房 「读不到」·不代表没有稿
读挂了(非 200 / 解析不出) 这一次没读回来 「暂时无法加载」·不代表没有稿
drafts 是空数组 读到了,现在确实没有 「现在没有等着审的稿」
truncated = true 读到了,但只是前 total 篇里的一部分 「还有 N 篇没列出来」

🚪 最后一行是 09-14 收尾时才补的,而它正是我自己那条判据的反例: 这份列表有上限(draftsLimit),本机库里 92 篇、回 50 篇、dropped=0 —— 页面收到的是一份看起来完整的列表,无从知道还有 42 篇。 当时线上只有 14 篇,所以没出事 —— 那不是安全,那是条件还没凑齐。 判据:有上限就必须有人把「被砍掉了」说出来;沉默的几种意思必须分得开, 「就这些了」也是其中一种。

🔑 不够格的人拿到的 v 是 null —— 和「压根没有这个键」长得一模一样,是故意的: 不告诉房外的人这儿有东西。

3.7.5 🔓 记者看得到线索的图 —— news_leads(2026-09-15)

主人 09-15 的原话:「我们的线索是内部的,内部自己的记者看的,不是发布。 你拿下来了,你阻止我看。记者要看图,这样才可以写嘛。」

他说的是对的,而卡住它的不是授权,是那条路是公开的: Discover 的数据走 db_overview 那把不带身份、谁都读得到的键。 在那条路上把图放开,不叫「内部人看得到」,叫把别人家的图发给全世界。

所以另开一条带身份的:hoopKv('data','news_leads') → LeadsForCaller, 权限仍然只有一个出处(newsroomRole,和审稿台、news_drafts 同一份 SQL、同一间房)。

走哪条路 谁读得到 图
db_overview(公开,不带身份) 任何人 照旧不给,image_held 说明为什么
news_leads(带身份) 只有新闻房里的人 一张不锁,抓到什么给什么

📄 正文也走这条路(2026-09-15 当天补的第二刀)。 主人紧接着报: 「我点开正文也看不到,我只是看到标题而已。我要对照那个新闻,我至少可以看嘛。」 ——我第一刀只塞了图和段数,没塞正文,那是漏的。现在 news_leads 带 body 和 n_chars。 公开那条从来不带正文,也不会带(forbiddenCols + 那条守卫),两件事不冲突: 我们抓回来的正文,自己读是一回事,往公开门口送是另一回事。

⚠️ 这一刀顺手改了一条老守卫,值得记: TestOverviewNeverSelectsBodyText 原来数的是「整份 news.go 里 body_text 出现几次」,写死 1 次。 内部那条路合法地多了一处,它当场红 —— 红得没道理。 数一份文件里的字数,分不出「公开门口吐正文」(要拦)和「带身份的内部路给正文」(该给)。 改成按函数切:latestNews 里一个字都不许有;LeadsForCaller 里随它几处; 总数必须等于「名单 1 处 + 内部那条几处」,多出来的第三处当场红。 三把刀各红:塞进公开那条 🔴 · 在第三个地方碰它 🔴 · 把禁列名单改掉 🔴。 (和 09-14 那次同一条判据:钉那条会被走到的路,不是钉一份文件。)

⚠️ 两件事必须分开,别合成一个开关(Gode 2026-09-15 点名): ① 内部记者看线索的图 —— 这条路给;② 把图对外发布 —— 另一回事,仍看 may_use_images。 所以每条线索上带一格 may_publish_image,页面照样显示图,但说得出 「这张只能内部看,还没核到对外发布的授权」。看得到 ≠ 发得了,两句话不能共用一个开关。 ⚠️ 绝不许用「把 26 家 may_use_images 全改 true」来解决 —— 那是把内部查看误写成公开使用授权(同上一条的判据:一个字段不能干两份活)。

守卫:leads_for_caller_db_test.go,同一个源、同一条线索,两条路读出相反结论。 分成两个文件写就没用了 —— 哪天有人改坏一边,另一边照样绿。四把刀各红: 授权闸塞回内部那条路 🔴 · may_publish_image 写死成 true 🔴 · 顺手把公开那条也放开 🔴(这一刀最危险的失手)· 不看是不是房里人 🔴。

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)—— ⚠️ 已被 §0.0 作废:现在只守 Crawl-delay,不读 Disallow(见 §3.10 注)
        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 条。

9. 🧭 开发步骤表 —— 每一步做什么、怎么算完、被谁挡着

Jeff 2026-08-14 语音:「清楚地写下来:接下来的技术、要开发的步骤,每一个步骤要开发什么, 到最后完成。」

⚠️ 写步骤不算开工(§0 那条「等 Fable 平台」仍然有效)。 而且正好相反:这张表让「到底哪几步真的被平台挡着」一眼可见 —— 在这之前,这件事只存在于对话里,没落在纸上。

「挡着」那一栏的读法:🟢 现在就能做 · 🟡 部分能做 · 🔴 等平台。

9.1 第一阶段:证明两件事(不接 AI)

# 做什么 完成的判据(可测) 依赖 挡着
A1 五种母版修到 Gallery 30 张全绿 30 张两档宽度全部无崩点,红框清零 — 🟢
A2 补 Quote / Timeline / Multi 三种母版 Gallery 加到 ~40 例仍全绿 A1 🟢
A3 Composition Engine(brightness / face_box / 安全区) 亮图、人脸左右各 3 例,文字都压得住 A1 🟢
A4 Motion(三下,≤200ms) 首次进视口播一次;prefers-reduced-motion 关掉后版面完整 A1 🟢
B1 四个一手源接通(BNM · Bursa · SGX · MAS) 各抓到 ≥ 10 条真公告,robots 全部检查并留证 — 🟡 只能本地跑,落库要等平台
B2 事件聚类(同一件事收拢) 人工核 50 条,聚错 ≤ 2 条 B1 🟡
B3 Evidence Pack(§3.11) 一条真事件,拿得出 ≥ 2 个独立来源 + 一手件 B2 🟡
B4 Source Lineage(谁先说的) 造一组「1 原创 + 3 跟发」,independent_sources 必须算成 1 B3 🟡

⚠️ A 和 B 两队并行,互不阻塞(Jeff 08-14 定)。 这一阶段的验收只有一句话:左边证明我们能「漂亮地告诉世界」,右边证明我们能「看到世界」。

9.2 第二阶段:接上 AI(平台好了才开工)

# 做什么 完成的判据 依赖 挡着
C1 数据契约落地(§8.1) 一份 schema 生成 Go struct + 模型约束;改字段只改一处 平台定库 🔴
C2 建表 + 迁移(§3.3) 迁移号从 LEDGER 取、跑通、可回滚 C1 🔴
C3 AI Journalist(以 claim 为单位输出) body_blocks 全部带 claim_ids,没有裸正文 C1 🔴
C4 七道闸门(§3.6) 故意喂三条坏的(编的数字 / 无出处 / 洽谈写成收购)必须全红 C3 🔴
C5 GREEN/YELLOW/RED 风险分级 RED 素材 Renderer 在代码层取不到 C4 🔴
C6 端到端跑通一条真新闻 一条真事件 → 卡片出现在手机上,出处可点回原文 C1–C5 🔴

9.3 第三阶段:进群(产品真正成立的地方)

# 做什么 完成的判据 依赖 挡着
D1 订阅 + 每日推送 同一条推给 100 人,news_deliveries 无重复行 C6 🔴
D2 Discuss / Save 甩进群、收藏,复用现有转发 C6 🔴
D3 Ask AI 上下文只有这条 doc + 源文本;答案视觉上和报道分开 C6 🔴
D4 撤稿 / 纠错闭环 从「有人报错」到「卡片消失」≤ 1 小时,且演练过一次 C6 🔴

9.4 ⚠️ 我认为现在真正缺的六件(Jeff 问「还缺什么」,这是答案)

  1. 🔴 到今天为止,机器一条真新闻都还没发现过。 所有信心建立在纸上和 30 张手写数据卡上。 B3 那一步(真事件 + Evidence Pack)才是这个项目第一个真正的里程碑。
  2. §2.2 那些质量指标,现在一条都测不了。 「事实锚定率 ≥ 95%」是猜的 —— 等 B3 跑起来才有资格定。在那之前它是占位,不是标准。
  3. 有「下架」,没有「怎么知道该下架」。 用户举报入口在哪?谁值班?半夜谁看? —— 这是出事时才会发现的洞,已列为 D4,而且要求演练过一次才算完成。
  4. Ask AI 的成本和滥用没算。 十个人对同一条各问五遍 = 五十次调用。 需要:同一条新闻的问答共享缓存 + 每人每小时上限。(D3 的前置)
  5. 没有「我们不做什么」的清单。 图纸只写要做的 —— 不写不做的,以后每个人都会往里加。(见 §7,但那份太短)
  6. 成本只有文字那块是实的。 终局量级我写的是「我没有数字」—— 那不是完善,是诚实的空白,要等 §6 第 15 条(一天处理多少条)拍板才能算。

9.5 一句话总结这张表

🟢 现在能做的只有 A 队四步(视觉)+ B 队本地那半截;🔴 剩下 14 步全部等平台。 所以「等平台」不是一句托辞 —— 它挡住的是这个项目 70% 的工作量。


📊 走到哪了(这一段是生成的,不是手写的)

一共 142 把刀,已完成 14 把。 更新于 2026-08-31。

队这一队在干什么几把刀已完成
A🎨 A 队 · Renderer 与视觉(不碰数据库,平台挡不住)380 ▱▱▱▱▱▱▱▱▱▱
B🕷️ B 队 · Discovery(抓取+解析,落文件不落库)372 ▰▱▱▱▱▱▱▱▱▱
C🚨 C 队 · 闸门与契约(纯函数,现在就能写+测)210 ▱▱▱▱▱▱▱▱▱▱
D🗄️ D 队 · 落库与上线(全部等平台)2911 ▰▰▰▰▱▱▱▱▱▱
E🧪 E · 验收、运营与善后171 ▰▱▱▱▱▱▱▱▱▱

✅ 已经做完的,每条都带提交号

刀做了什么怎么做的证据提交
D-2926 家名单真灌进生产库(桌子建好了,东西也放上去了)迁移 000322 给 news_sources 补自然键 key(000320 建表时我漏了,没它载入脚本没法重跑);文件→库单向,YAML 是唯一出处,库里手改会被下次灌冲掉生产库 26 家:MY 14 / SG 7 / GB 3 / US 2;一手件 14 / 媒体 9 / 通讯社 3。三个权利闸门全 0(文字可用 0 · 可存全文 0 · 不需授权 0)。跑第二遍仍是 26 家、updated_at 07:01:15→07:01:30(证明真跑了不是跳过)。坏刀:硬插一条重复的 key='cna' 当场报 duplicate key,数仍 26f52a68b9
E-17逐刀验收页 —— 他自己点得进去看每一刀走到哪进度是算出来的(已完成几刀直接数);每条带提交号+证据;写早了/搁置的照实列;唯一出处 news_progress.yamlhttps://hoop-docs.pages.dev/news 的 🔪 逐刀清单;线上抓过那句「一共 141 把刀」核实,不是看部署回显06c927f8
D-01, D-02, D-03, D-04, D-05, D-06, D-07, D-08, D-09, D-25新闻平台 12 张表(源/类别/条目/事件/证据/claim/稿件/稿件用了哪些源/版本历史/订阅/推送)迁移 000320,已在生产库真跑;权利六项默认全「不许」;三条守卫全部坏刀验红过生产库 schema_migrations=320 dirty=f;12 张表逐个查出来过;NOVA 用他自己的连接独立复核过一遍2cd6dae2
B-01Source Atlas —— 上哪儿看的名单,马新两地 26 家字段和 news_sources 一一对应;26 家权利六项全 false;守卫每把刀验在它该红的那一句上真打了 26 个地址,22 个通;不通那 4 个是对方拦脚本,不是名单写错14d87833
B-02把 26 家门口那张告示(robots.txt)抄回来存证按 RFC 9309 解析;取不到一律记 unknown 并按「不许」办;原文一起存24/26 拿到;Reuters 对没被点名的机器人整站禁止(打开原文核过);6 家自己写了间隔993af94a
(不占刀号)每家一行的规矩表:能不能抓 / 它说几秒 / 我们用几秒 / 禁了几条路径只整理事实,不执行拦截;数据只从 atlas 和 robots 摘要两处来artifacts/news_robots/rules.csv,26 行a6ebef6a

⏸️ 写早了、搁着不接的

  • B-07 限速闸门(同一家网站多久去一次) —— Jeff 2026-08-31 语音:这一步只做数据,闸门是写爬虫那一刀才写的。代码留着但没有任何一条路调用它 (0d840830)

👉 下一刀

  • B-03 同一家的告示 30 天内不重复去要(省得反复打扰人家)
  • B-04, B-05 给我们的机器人定个正式名字 + 一页说明「我们是谁、为什么抓、怎么联系、怎么叫我们别抓」

📋 26 家的规矩表(能不能抓 · 多久去一次)

⚠️ 「能不能抓」只说机器人能不能读,不代表它的文字能进我们的产品 —— 那要读完各家条款,那一刀还没做,所以最后一列现在全是「不许」。

源国档能不能抓它自己说我们实际用禁了几条路径文字能进产品?
CNA (Channel NewsAsia)SGmediarobots 没禁1010 秒29不许
The Straits TimesSGmediarobots 没禁1010 秒86不许
The Business Times (Singapore)SGmediarobots 没禁1010 秒1不许
Bank Negara MalaysiaMYprimaryrobots 没禁—5 秒1不许
Securities Commission MalaysiaMYprimaryrobots 没禁—5 秒1不许
Department of Statistics MalaysiaMYprimaryrobots 没禁—5 秒1不许
Ministry of Finance MalaysiaMYprimaryrobots 没禁—5 秒14不许
Ministry of Investment, Trade and Industry MalaysiaMYprimaryrobots 没禁—5 秒0不许
Prime Minister's Office of MalaysiaMYprimaryrobots 没禁—5 秒1不许
Singapore ExchangeSGprimaryrobots 没禁—5 秒0不许
Ministry of Trade and Industry SingaporeSGprimaryrobots 没禁—5 秒1不许
London Bullion Market AssociationGBprimaryrobots 没禁—5 秒1不许
World Gold CouncilGBprimaryrobots 没禁—5 秒33不许
Associated PressUSwirerobots 没禁—5 秒18不许
The Edge MalaysiaMYmediarobots 没禁—5 秒0不许
The StarMYmediarobots 没禁—5 秒8不许
New Straits TimesMYmediarobots 没禁—5 秒1不许
Astro AwaniMYmediarobots 没禁—5 秒16不许
Free Malaysia TodayMYmediarobots 没禁—5 秒12不许
Malay MailMYmediarobots 没禁—5 秒16不许
Monetary Authority of SingaporeSGprimaryrobots 没禁22 秒0不许
Singapore Department of StatisticsSGprimaryrobots 没禁22 秒1不许
Bernama(马来西亚国家通讯社)MYwirerobots 没禁11 秒2不许
ReutersGBwirerobots 说不许—5 秒1不许
Bursa MalaysiaMYprimary未知(按不许办)—5 秒0不许
CME Group (COMEX 黄金期货)USprimary未知(按不许办)—5 秒0不许

10. 🔪 逐刀清单 —— 从第 1 刀到第 141 刀,按做的顺序排

Jeff 2026-08-15 语音(第二次,他退回了我的排法): 「我希望每一刀是我可以看得到、可以跟进你的开始。比如前面几刀,我肯定应该会要你先创建数据库, 然后把数据库全部创建好,接下来的刀才是写 Python 去抓那些新闻。 不要突然间跳跃直接看结果 —— 从头开始一步一步一步做。如果你不是这样,你要重新排序这些刀的步骤。」

他说得对,我第一版排错了,错在哪值得写下来: 我是按「哪些现在不被平台挡」排的 —— 那是我方便,不是他能跟进。 按那个排法,他看到的第一刀是「修 Gallery 的红框」,他根本不知道这跟新闻有什么关系。 判据:一份清单是给做的人排的,还是给看的人排的?

这份清单的四条规矩: 1. 一刀 = 一个人一次能做完的活。 「做好渲染器」不是一刀,「Data 母版数字长度算法回归」才是。 2. 每一刀都有「怎么算完」,而且可测。 不写「做好」「优化」这种。 3. 刀号(A-01 / B-13)是身份证,永不重排;重排的是「第几刀」。 —— 不然你昨天看过的那份和今天这份对不上,那比乱序更糟。 4. ⚠️ 我没有为了凑数把刀数撑大。 该多少刀就多少刀 —— 凑出来的刀数会让你以为工作量比实际大,那比少写更糟。

挡不挡:🟢 现在就能做 · 🟡 一半能做 · 🔴 等平台(Fable 的小程序平台没定,库建在哪不知道)。

⚠️ 照这个顺序看,你会发现前 9 刀全是 🔴。 我没有把它们藏到后面 —— 藏起来你就看不见「等平台到底卡住了什么」。真相就是:按正常顺序,我们卡在第一刀。 好消息在第二步:抓取那一整段只往文件里写,不碰库,所以它可以插队先跑,库建好了再灌进去(第 46 刀)。

👁 那一列 = 这一刀做完,你能亲眼看到什么。 Jeff 2026-08-15 语音:

「你的数据库已经建成了 → 你的机器人已经跑动了 → 接下来可以拉到第一条新闻了 → 现在新闻可以开始编辑了 → 编辑出来我们可以显示了。每一步我都可以验收成果。」

⚠️ 👁 是「—」的那些刀,我没编一个假画面出来。 它们是给后面垫的(建一张空表、写一个纯函数)。 每行都填满看起来更漂亮,但那是假的 —— 到时候你按着去验,验不出来。


10.0 📮 2026-08-15 第二轮:Jeff 逐段审完,改了 6 处

他原话:「不用重排八九成,但不是完美。我会改 6 个地方。」逐条落地情况:

他说的 我做了什么
① 第 1–9 刀 PASS,不动 ✅ 没动。但我往里塞了一刀(版本表)—— 理由在下面
② 第 10–19 刀防守过多,第一条新闻来得太慢 ✅ 插 B-36「HOOPBot First Fetch」到第 16 刀,一屏冒烟报告 + 10 条真标题
③ 第 26–39 刀 PASS ✅ 没动
④ 缺 ScoutBot(谁负责发现新来源) ✅ 加 B-37(自己找源)+ D-26(候选只能是 discovery_only,你点 Approve 才升级)
⑤ 第 58 刀倒依赖,只做元数据闸 ✅ C-10 改成不做识别、unknown 默认拦;人脸识别留到 A-17。现在全表倒依赖 = 0
⑥ 第 69–107 太久才验收一次 ✅ 加四个中途验收点 E-13~E-16(第 80 / 89 / 95 / 112 刀)
⑦ 缺 Correction / Version History ✅ 加 D-25 版本表 + D-27 更正接口 + D-28 UPDATED/CORRECTED 标 + 版本对比

🟠 一处我顶回去了,请你看一眼: 你说「第 1–9 刀不动」,但 Correction 的版本表必须在建库那一步就建。 加在最后 = 那时候要回头改第 6 刀建好的表,多一次迁移。 理由就是你自己那句「从底层开起」—— 底层没留位置,上层补不出来。 所以建库从 9 刀变 10 刀(多的那刀是 D-25)。

🟠 刀数是 140,不是你估的 132–133 —— 多出来的 7 刀我说清在哪: 你列的三类新东西,真做起来不止一刀:Correction 是 3 刀(表 / 接口 / 用户侧标记与对比), ScoutBot 是 2 刀(找源 / 审核升级),中途验收点你点了 5 个、其中第 107 那个本来就有,所以新加 4 刀。 3+2+1(First Fetch)+4 = 10 刀,140。每一刀都能指回你信里的哪一句,没有一刀是我自己塞的。 你觉得哪刀是凑的,点名我砍。


⏪ = 这一刀依赖后面才做的刀。这一轮是 0 处 —— 你那条第 5 修法真的把路捋直了。

10.1 👀 第零步:先让他看得见(不然后面每一刀他都在盲等) —— 第 1–1 刀

Jeff 2026-08-31 语音问「这 140 刀里有没有一刀是做一个我能进去看、能逐步验收的列表」——当时没有,是我漏的。他 08-15 就说过「每一刀我可以看得到、可以跟进你的开始」,我只拿那句话排了顺序,没把「能看」本身做成一件要交付的东西。现在补成第零步。

第几刀 刀号 做什么 怎么算完(可测) 👁 你能看到什么 挡
1 E-17 逐刀验收页:每一刀走到哪、谁做的、证据在哪,他自己点得进去 进度那一段是算出来的(已完成几刀直接数,不是手写);每条带提交号 + 证据;写早了/搁置的也照实列;唯一出处是 news_progress.yaml,手改网页会被下次生成冲掉 — 🟢

10.2 🗄️ 第一步:先决定东西存在哪(建库) —— 第 2–11 刀

他说的就是这个:「肯定应该会要你先创建数据库,然后把数据库全部创建好」。这十刀全是 🔴 —— 因为 Fable 的小程序平台没定,我们不知道表建在哪个库里。这不是我排给你看的,这就是现实:照正常顺序做,第一刀就得等。

第几刀 刀号 做什么 怎么算完(可测) 👁 你能看到什么 挡
2 D-01 迁移号取号 + 建 news_sources 从 LEDGER 取号并 +1 写回 — 🔴
3 D-02 建 news_categories 四到六类落表 — 🔴
4 D-03 建 news_items UNIQUE(source_id, external_id) 空表建好,能 INSERT 一条假新闻再查出来 🔴
5 D-04 建 news_events(事件为单位) 事件先于文章存在 — 🔴
6 D-05 建 news_articles(doc jsonb + 投影列) headline 等是 GENERATED — 🔴
7 D-06 建 news_article_sources 多对多,防幻觉的锚 — 🔴
8 D-07 建 news_evidence / claims claim 级留痕 — 🔴
9 D-08 Source Registry 表(权利六项) may_use_text 等是可索引的列 — 🔴
10 D-09 回滚演练 迁移能回滚,数据不丢 — 🔴
11 D-25 建 news_article_versions(版本历史表) 同一条新闻存得下 V1/V2/V3,每版带时间 + 改动理由 同一条新闻塞进 V1 和 V2,两版都查得到 🔴

10.3 🕷️ 第二步:写 Python 去抓新闻 —— 第 12–28 刀

他说的第二段:「接下来的刀才是写 Python 去抓那些新闻」。这一整段 🟢 —— 它只往文件里写,不碰库,所以可以插队先做。先有一份「上哪儿抓」的名单(Source Atlas),再有「许不许抓」(robots),再有「别把人家抓垮」(限速),最后才是真的去抓。

第几刀 刀号 做什么 怎么算完(可测) 👁 你能看到什么 挡
12 B-01 Source Atlas 先做成一份 YAML 文件 马新两地 20+ 源,字段齐(§3.14.2) 一份 YAML,20+ 个马新新闻源摊开在你面前 🟢
13 B-02 robots.txt 抓取 + 解析(RFC 9309) 对 20 个源各查一次,结果落文件 20 个源的 robots 判定表:哪些 ALLOWED、哪些 BLOCKED 🟢
14 B-03 robots 缓存 + 复查期 同一 domain 30 天内不重抓 robots — 🟢
15 B-04 HOOPBot User-Agent 定名 UA 串定稿,带 +https://…/bot — 🟢
16 B-36 HOOPBot First Fetch —— 冒烟一次,证明蜘蛛真的活了 一屏打出:Source=Bernama · robots=ALLOWED ✓ · UA=HOOPBot ✓ · rate=1/5s ✓ · FOUND 10 ITEMS + 10 条真标题 🔥 一屏冒烟报告 + 10 条真标题 —— 蜘蛛第一次真的活了 🟢
17 B-05 /bot 说明页(静态 HTML) 我们是谁/为什么抓/联系/opt-out/IP 段 — 🟢
18 B-06 opt-out 自助表单(静态版,先收邮件) 提交后有回执,名单落文件 — 🟢
19 B-07 DomainGate:令牌桶限速 同 domain 5 秒 1 次,并发 ≤2 — 🟢
20 B-08 DomainGate:全局唯一(四 bot 共用) 四个 bot 同时跑,合计不超过限速 — 🟢
21 B-09 DomainGate:错误预算 + 自动暂停 连错 20 次 → 暂停 6 小时 + 报警 — 🟢
22 B-10 DomainGate:402 当永久状态 遇 402 标 needs_licence,不重试 — 🟢
23 B-11 RSS 抓取器 Bernama/CNA/ST 各抓一次,条目落文件 — 🟢
24 B-12 Sitemap 抓取器 从 sitemap 发现新 URL — 🟢
25 B-13 BNM 公告抓取 抓到 ≥10 条真公告 BNM 的 10 条真公告标题 🟢
26 B-14 Bursa 公司公告抓取 抓到 ≥10 条真公告 Bursa 的 10 条真公告标题 🟢
27 B-15 SGX 公司公告抓取 抓到 ≥10 条真公告 — 🟢
28 B-16 MAS media release 抓取 抓到 ≥10 条真公告 — 🟢

10.4 🧩 第三步:看懂抓回来的那一堆 —— 第 29–34 刀

抓回来是一坨 HTML。这一段把它变成「标题/时间/正文」,去掉重复的,再把讲同一件事的几篇聚成一个事件 —— HOOP NOW 的单位是事件,不是文章。

第几刀 刀号 做什么 怎么算完(可测) 👁 你能看到什么 挡
29 B-17 HTML → 结构化 JSON(评估 news-crawler-LM) 20 篇抽样,标题/时间/正文准确率 ≥90% 同一篇:左边原始 HTML,右边解析出来的 JSON 🟢
30 B-18 原文快照保存(本地) 每条存 HTML + 抓取时间 + URL — 🟢
31 B-19 去重(同 external_id) 同一条抓两次只留一条 — 🟢
32 B-20 聚类:标题/正文相似度 人工核 50 条,聚错 ≤2 「讲同一件事的 4 篇」被自动归成一堆 🟢
33 B-21 聚类:实体识别(公司/机构) 抽 30 条,实体准确率 ≥85% — 🟢
34 B-22 事件对象成形(不是文章) 一个事件带 N 个来源条目 一个事件卡:下面挂着 N 个来源 🟢

10.5 🧬 第四步:查它从哪来、够不够格发 —— 第 35–42 刀

同一条消息被十家转发,不等于十个来源。这一段查血缘(谁是原创、谁是跟发),再给每个事件攒一份 Evidence Pack(每句话挂着它的证据)。

第几刀 刀号 做什么 怎么算完(可测) 👁 你能看到什么 挡
35 B-23 Lineage:canonical link 提取 有 canonical 的都提到 — 🟢
36 B-24 Lineage:发布时间排序定 origin 造一组 1 原创 3 跟发,origin 判对 一条消息的传播链:谁原创、谁跟发 🟢
37 B-25 Lineage:引用关系识别(文中「据路透」) 抽 20 条,识别率 ≥80% — 🟢
38 B-26 Lineage:independent_sources 计数 上面那组必须算成 1 — 🟢
39 B-27 Evidence Pack 组装 一条真事件,claims 各自挂证据 📄 一份 Evidence Pack:每句话下面挂着它的证据 🟢
40 B-28 Evidence:conflicts 检测 造一组互相矛盾的,必须标出来 — 🟢
41 B-29 confidence 分档(high/med/low) 由来源数+一手件+冲突推出,不用小数 — 🟢
42 E-03 B3 里程碑:一条真事件 + Evidence Pack ≥2 独立来源 + 一手件 🏁 机器第一次自己发现一条真新闻,并且拿得出证据 🟢

10.6 🏢 第五步:主动去盯,而不是等着别人发 —— 第 43–51 刀

前面是「捡」,这一段是「盯」:盯 50 家公司的公告页、盯监管机构,有变化五分钟内发现。最后一刀把前面抓的全灌进库 —— 那一刀要等第一步的库先建好。

第几刀 刀号 做什么 怎么算完(可测) 👁 你能看到什么 挡
43 B-30 Company Universe 名单(先 50 家) 每家一份地址簿(官网/IR/newsroom) — 🟢
44 B-31 Company 地址簿有效性检查 连续 404 标 needs_review — 🟢
45 B-32 MonitorBot:盯页面变化 Bursa 某页有变时 5 分钟内发现 Bursa 页面一变,5 分钟内跳出来告诉你 🟢
46 B-33 SourceHuntBot:发现事件后找第二来源 给一个事件,自动找到 ≥1 个别的来源 — 🟢
47 B-37 ScoutBot:给一个公司名,自己找出它的新闻源 给 10 家不在 Atlas 里的公司,≥8 家自动找对官方 newsroom / IR / rss / sitemap 并产出候选 Source Profile;只进待审,绝不自动进生产抓取 🔎 给它 10 家公司名 → 一张「找对了几家」的成绩单 + 候选 Source Profile 🟢
48 B-34 本地跑批脚本 + 日报 跑一次,出「抓了多少/聚成几个事件/几个够格」 每天一份日报:抓了多少、聚成几个事件、几个够格发 🟢
49 B-35 ⚠️ 落库(等平台定库) 事件与证据进真表 — 🔴
50 D-26 候选源审核界面:Approve → Source Registry ScoutBot 产出的候选只能是 discovery_only,人点一下才升级 候选源审核页,你点 Approve 才进 Registry 🔴
51 D-10 抓取结果从文件灌进库 本地跑批的产物一次导入 — 🔴

10.7 📐 第六步:定死「一条新闻长什么样」 —— 第 52–56 刀

这是整个系统的契约:一份 Schema。定不下来,后面所有闸门都没有量尺。

第几刀 刀号 做什么 怎么算完(可测) 👁 你能看到什么 挡
52 C-01 框架 JSON Schema 定稿 40 份手写 doc 全部校验通过 一份 Schema + 40 份样例全部校验通过的报告 🟢
53 C-02 Schema → Go struct 生成脚本 改 schema 一处,struct 跟着变 — 🟡
54 C-03 Schema 当模型结构化输出约束(离线试) 不接生产,先用假 key 验证格式 — 🟢
55 C-14 claim → 正文组装函数 没有 claim_ids 的正文块直接拒绝 — 🟢
56 C-15 attribution: fact / claimed_by 渲染规则 claimed_by 强制带主语 — 🟢

10.8 🚨 第七步:装闸门 —— 不许胡说 —— 第 57–70 刀

这一段是这个产品能不能做的分水岭。 每一刀都是纯函数:输入一段文本,输出过不过。跟数据库没关系,现在就能写、能测。

第几刀 刀号 做什么 怎么算完(可测) 👁 你能看到什么 挡
57 C-04 闸门 1:有出处 无出处必 blocked — 🟢
58 C-05 闸门 2:数字锚定(key_numbers 逐个查源) 喂一条编的数字,必红 喂一个编的数字进去,当场变红 🟢
59 C-06 闸门 3:专名锚定(entities 逐个查源) 喂一个编的人名,必红 — 🟢
60 C-07 闸门 5:长度 超阈值必红 — 🟢
61 C-08 闸门 7:fact_level 五级 + 更强动词词表 「洽谈」写成「已收购」必红 把「洽谈」写成「已收购」,当场变红 🟢
62 C-09 闸门 7 词表放配置、可被非工程改 改词表不用改代码 — 🟢
63 C-10 闸门 4:图片权利/人脸元数据闸(不做识别) 喂 contains_face=true 或 rights=unknown → 图必被拦;unknown 默认拦,不默认放 — 🟢
64 C-11 闸门 6:单源孤证 + 敏感类 单源 + 犯罪类必红 — 🟢
65 C-12 gate_report 结构(逐项留证) 每条稿带完整闸门结果 — 🟢
66 C-13 闸门总装 + 三条坏样本回归 编的数字/无出处/洽谈写成收购,全红 🚨 三条坏样本的拦截报告,全红 🟢
67 C-16 risk_tier 推导(auto / human_review) 由类别+conflicts+fact_level 推出 — 🟢
68 C-17 GREEN/YELLOW/RED 内容分级 六类样本各判对 — 🟢
69 C-18 图片分级 + RED 素材在代码层取不到 构造一个 RED 素材,Renderer 拿不到 构造一个 RED 素材,Renderer 根本拿不到它 🟢
70 E-02 闸门三条坏样本 = C 队验收 全红 🚨 三条坏样本全被拦下 🟢

10.9 🤖 第八步:让 AI 来写(等平台) —— 第 71–73 刀

到这里才轮到 AI。顺序是故意的:先有闸门,再放 AI 进来 —— 反过来就是 Apple 那个下场(§1.2)。

第几刀 刀号 做什么 怎么算完(可测) 👁 你能看到什么 挡
71 C-19 ⚠️ AI Journalist 接线(等平台) 以 claim 为单位输出 AI 写出来的第一条稿,每句挂着 claim_id 🔴
72 C-20 ⚠️ AI Fact Check 接线(等平台) 逐句检查依据 — 🔴
73 C-21 ⚠️ AI Editor 否决权(等平台) 标题和 claims 对不上就打回 — 🔴

10.10 🎨 第九步:把它画成一张卡(五种母版) —— 第 74–89 刀

AI 只说「这是什么新闻」,长什么样由 Renderer 决定(§3.0)。这一段做五种母版 + 三个新的。

第几刀 刀号 做什么 怎么算完(可测) 👁 你能看到什么 挡
74 A-01 五种母版:把 Gallery 30 张修到全绿 30 张 × 285/334 两档,红框清零 Gallery 30 张全绿 🟢
75 A-02 Breaking 母版:hero 高度下限 + 标题夹 3 行的边界回归 标题 1/3/5 行各一例,图都还在 — 🟢
76 A-03 Big Story variant A(大横图) 有 hero 且 hero_quality 高时选中 A — 🟢
77 A-04 Big Story variant B(人物版,左文右图) face_box 在右半时选中 B,文字不压脸 — 🟢
78 A-05 Big Story variant C(大数字版) key_number 强 + 图弱时选中 C — 🟢
79 A-06 variant 选择函数写成纯函数 + 单测 给 12 组输入,断言选中哪个 variant — 🟢
80 E-13 验收点:Big Story 三个 variant 摆一起给你看 A/B/C 三张并排,同一条新闻 Big Story 三个 variant 并排 🟢
81 A-07 Data 母版:数字长度算法回归 ≤4/5-7/8-10/>10 四档各一例,都不换行 — 🟢
82 A-08 Data 母版:紧凑记号显示原值 压缩时下方必须出现「原值 …」 — 🟢
83 A-09 Data 母版:图表缺数据时的空态 chart=null 时不画空坐标轴 — 🟢
84 A-10 Explainer:2/3/5 步三种长度 三种都不撑破 285pt — 🟢
85 A-11 Normal:有图/无图两版 无图版不留灰占位 — 🟢
86 A-12 Quote 母版(第 6 种) 引言型不再落 normal,署名独立样式 — 🟢
87 A-13 Timeline 母版(第 7 种) ≥3 项时间轴,超 6 项折叠 — 🟢
88 A-14 Multi-news 母版(第 8 种) 2×2 瀑布流,引用 ≥3 条别的 article — 🟢
89 E-14 验收点:八个母版第一次全部摆在你面前 8 张一屏,你一眼看出它们是不是同一套系统 🎨 八个母版第一次全部摆在你面前 🟢

10.11 🖼️ 第十步:图怎么配、动效怎么走 —— 第 90–100 刀

亮图上压白字会看不清、人脸会被文字压住 —— 这些不是审美,是 Gallery 真崩出来的(§3.5C)。

第几刀 刀号 做什么 怎么算完(可测) 👁 你能看到什么 挡
90 A-15 Composition:brightness 阈值定档 亮图 3 例、暗图 3 例,文字都读得清 — 🟢
91 A-16 Composition:brightness 入库时预计算(先写函数) 给一张图算出 0-1 的值,同图两次结果一致 — 🟢
92 A-17 Composition:face_box 自动识别 → 文字排左/排右(顺带把 C-10 从「元数据」升成「自动」) 人脸左右各 3 例文字都不压脸;且 C-10 从此拿得到真的 contains_face — 🟢
93 A-18 Composition:焦点裁切(focal_point) 同一张图三种比例裁切,主体都在 — 🟢
94 A-19 Composition:图片复杂度 → 改上下结构 复杂图不 overlay 文字 — 🟢
95 E-15 验收点:图片智能排版 同一张图 → 亮/暗/人脸左/人脸右 四种处理,四张对照 同一张图四种处理的对照 🟢
96 A-20 Motion:image fade ≤80ms,首次进视口播一次 — 🟢
97 A-21 Motion:headline reveal ≤80ms,跟在 fade 之后 — 🟢
98 A-22 Motion:key number pop ≤60ms;三段总时长 ≤200ms — 🟢
99 A-23 Motion:重渲不重播 切宽度/切主题不重放 — 🟢
100 A-24 Motion:尊重 prefers-reduced-motion 关掉动画后版面完整 — 🟢

10.12 📄 第十一步:详情页、第二张卡、品牌 —— 第 101–112 刀

卡片是 3 秒,详情页是 3 分钟。第二张卡是「这件事跟你有什么关系」。

第几刀 刀号 做什么 怎么算完(可测) 👁 你能看到什么 挡
101 A-25 Story 详情页骨架(01~05 五段) 五段全部由字段驱动,没有硬写文字 — 🟢
102 A-26 Story:05 Source 那一屏做显眼 出处按钮在首屏可见,不用滚 — 🟢
103 A-27 第二张卡:why_talking 渲染 second_card 存在时出第二张 — 🟢
104 A-28 第二张卡:what_it_means 渲染 同上,标题不同 — 🟢
105 A-29 第二张卡:bull_vs_bear 渲染 + 非投资建议标 正反两栏,底部硬标免责 — 🟢
106 A-30 density:minimal / standard / rich 三档 同一条新闻三档各渲一次,长相明显不同 — 🟢
107 A-31 品牌:五个分类色定档 World/Business/AI/Sport/Local 各一色,对比度达标 — 🟢
108 A-32 品牌:字号阶梯落成 CSS 变量 改一处,全站字号跟着变 — 🟢
109 A-33 品牌:圆角/间距/栅格落成变量 同上 — 🟢
110 A-34 暗色模式 30 张在暗色下重渲,全绿 — 🟢
111 A-35 无障碍:对比度检查 正文对比度 ≥ 4.5:1 — 🟢
112 E-16 验收点:完整 HOOP Visual System 品牌色/字号/圆角/暗色/无障碍 一页看全 一页看全整套视觉系统 🟢

10.13 🧪 第十二步:Gallery 当考卷 —— 第 113–116 刀

你 08-14 定的:30 张全部漂亮,Renderer 才算过关。 这一段把考卷扩到 40 张并自动化。

第几刀 刀号 做什么 怎么算完(可测) 👁 你能看到什么 挡
113 A-36 Gallery 扩到 40 例(补 Quote/Timeline/Multi) 新母版每种 ≥3 个极端例 — 🟢
114 A-37 Gallery 一键截图脚本 跑一条命令出全部图,不用手点 — 🟢
115 A-38 Renderer 冒烟测试(纯函数层) 给 40 份 doc,全部返回非空 HTML 且无异常 — 🟢
116 E-01 Gallery 全绿 = P0-A 验收 40 张两档全过 🎨 Gallery 40 张最终考试 🟢

10.14 📲 第十三步:发出去(进群 / 推送 / Ask AI) —— 第 117–132 刀

到这里用户才第一次看见 HOOP NOW。 前面 107 刀他一眼都看不到 —— 这就是为什么要有 Gallery 和 /cards:让你在这之前就能看见。

第几刀 刀号 做什么 怎么算完(可测) 👁 你能看到什么 挡
117 D-11 发布接口 + status 流转 draft→passed→published→retracted 📲 第一张卡真的出现在群里 🔴
118 D-12 撤稿接口 一条命令让卡片消失 — 🔴
119 D-27 更正/更新接口(V1→V2,必须填理由) 「死亡 4 人」→「官方更新为 7 人」不是重发,是同一条的 V2;不填理由发不出去 「死亡 4 人」→「官方更新 7 人」变成同一条的 V2 🔴
120 D-28 用户侧:UPDATED / CORRECTED 标 + 点进去看改了什么 群里那张卡上出现「UPDATED 12:41」,点开能看到 V1 和 V2 差在哪一句 📲 群里那张卡上出现「UPDATED 12:41」,点得进版本对比 🔴
121 D-13 订阅表 + 订阅界面 用户能挑类别 — 🔴
122 D-14 推送扇出 + 幂等 推给 100 人无重复行 — 🔴
123 D-15 Renderer 进 App(WebView 渲同一份 HTML) 手机上和网页上长一样 — 🔴
124 D-16 Discuss(甩进群) 复用现有转发 — 🔴
125 D-17 Save(收藏) news_saves 表 — 🔴
126 D-18 Ask AI:上下文只有本条 doc + 源文本 问训练里的旧事实,它答不出来 — 🔴
127 D-19 Ask AI:问答共享缓存 同一条第二个人问,不重复调用 — 🔴
128 D-20 Ask AI:每人每小时上限 超限有友好提示 — 🔴
129 D-21 Ask AI:回答视觉区分 + 「AI 的看法」标 和报道正文一眼分得开 — 🔴
130 D-22 Live 管道:一手件类实时发布 Bursa 公告 5 分钟内进群 — 🔴
131 D-23 Live 节流 同类每小时 ≤1 条 — 🔴
132 D-24 Daily Brief 06:30 生成 / 07:30 推送 定时任务跑通 📲 早上 07:30,第一份 Daily Brief 推到手机上 🔴

10.15 🧯 第十四步:出错怎么办 —— 第 133–141 刀

新闻产品一定会发错。 这一段不是善后,是上线的前提:纠错入口、值班、撤稿演练、一个能整个关掉的开关。

第几刀 刀号 做什么 怎么算完(可测) 👁 你能看到什么 挡
133 E-04 质量指标先定测法(不定及格线) 六个指标各写清「怎么量」 — 🟢
134 E-05 跑三十天再定及格线 用真数据定,不用猜的 — 🔴
135 E-06 纠错入口(用户报错) 卡片上有「报错」按钮 — 🔴
136 E-07 值班与响应流程 谁看、多久内响应,写下来 — 🔴
137 E-08 更正演练 + 撤稿演练各一次 更正:群里那张卡当场变成 UPDATED 且点得进版本对比;撤稿:从报错到卡片消失 ≤1 小时 从报错到卡片更正 / 消失,全程演一遍给你看 🔴
138 E-09 整功能关掉的开关 一个开关让 HOOP NOW 整个下线 一个开关,让 HOOP NOW 整个下线 🔴
139 E-10 成本看板 每天花了多少,按类别拆 — 🔴
140 E-11 「我们不做什么」清单 写下来并贴在图纸里 — 🟢
141 E-12 律师看一次条款 拿到书面意见 — 🟡

10.16 📊 一共 141 刀,其中 36 刀你能亲眼验收

刀数 占比
🟢 现在就能做 101 71%
🟡 一半能做 2 1%
🔴 等平台 38 26%

你问「到多少刀就可以做完」——141 刀。 36 刀(占 25%)做完当场有东西给你验 —— 下面是那条「从底层长上来」的线, 就是你语音里说的那五段:

第几刀 那一刻你能看到什么
第 4 刀 🗄️ 「你的数据库已经建成了」 —— 空表能塞一条进去再查出来
第 16 刀(B-36) 🔥 「你的机器人已经跑动了」 —— 一屏冒烟报告 + 10 条真标题
(你要的那个爽点 —— 原来第一条真新闻要等到第 23 刀)
第 39 刀(B-27) 📄 「可以拉到第一条新闻了」 —— 一份 Evidence Pack,每句话挂着证据
第 42 刀(E-03) 🏁 机器第一次自己发现一条真新闻,并且拿得出证据
在这之前,我们所有的把握都建立在纸上
第 71 刀(C-19) ✍️ 「现在新闻可以开始编辑了」 —— AI 写的第一条稿,每句挂着 claim_id
(注意它排在闸门后面 —— 先造监狱,再放 AI 进去)
第 89 刀(E-14) 🎨 八个母版第一次全部摆在你面前
第 116 刀(E-01) 🎨 Gallery 40 张最终考试 —— 你 08-14 定的那条
第 117 刀(D-11) 📲 「编辑出来我们可以显示了」 —— 第一张卡真的出现在群里
第 120 刀(D-28) 🔄 群里那张卡变成「UPDATED 12:41」,点得进版本对比
(你加的那把刀:新闻不是只有「对→发布 / 错→删除」)
第 132 刀(D-24) 📲 早上 07:30,第一份 Daily Brief 推到手机上

⚠️ 注意第 117 刀之前,普通用户一眼都看不到 HOOP NOW。 这正是我先做 /cards 和 /gallery 的原因 —— 让你在那 116 刀之前就能看见它长什么样,而不是等到最后一刻才发现画错了。

⏪ 顺序上的取舍(共 0 处,我不藏): · 没有 —— 每一刀的依赖都排在它前面。

这份清单是脚本生成的(tools/news_cuts_gen.py)—— 手写一百多条必然错号。 改内容改脚本再跑;脚本里有三道闸门,都故意跑坏过一次确认它真的会红: 漏排一刀会红 · 排重一刀会红 · 👁 那栏写了一个不存在的刀号会红。 ⏪ 那一行也是脚本自己算出来的 —— 不是我说「没有倒依赖」,是它数出来 0 处。


要你拍板的 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_type、status、body_blocks、entities、schema_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 那几条,拍完板再谈动手。