0.0 ⛓️ 三条铁律 —— Jeff 2026-08-31 亲自定的,绝不违背
他原话:「把这三条铁律写下来!我们绝不违背 也绝对这样执行!」 这一节放在最前面,因为它管着后面每一行代码怎么写。改这三条要他本人开口。
第一道 · 发现 —— 我们只是去看
追求新闻真相,任何人、任何告示都不该让我们停下脚步。
robots.txt 那种告示是礼貌,不是法律;我们的抓取只是求知欲。公开看得到的东西,我们去读。
人家要封我们,那是他的权力 —— 但他不能阻止我们。
这一道唯一的边界(线过去了性质就变): · 不用别人的账号,不破解,不钻漏洞白看。 · 需要登录/付费才看得到的,我们正正当当去注册、去订阅、去付费,用自己的账号读 —— 我们就是合法读者,那不叫绕过,那叫花钱看。 · 这一条不是礼貌问题,是法律:马来西亚《计算机犯罪法 1997》第 3 条、新加坡《计算机滥用法》 管的都是「未经授权取用」。门开着走进去,和门锁着撬开,是两件事。
⚠️ 现实代价(不是道德问题,是生意):不理告示的代价通常不是被告,是被封 IP —— 那个源就永远失去了。该守的守、该谈授权的去谈,别为了省一步把最有价值的路堵死。
第二道 · 编写 —— 我们自己写,自己负责
尊重版权。绝不搬抄任何人的内容上架。自己写、自己交叉比对、对自己的内容负责。
⚠️「让 AI 把人家的报道改写一遍」不算自己写。 换了词,它仍然是从人家那篇长出来的。 真正的自己写 = 从事件出发,回到一手件(部长的原文告示、交易所公告、统计局数字)自己组织。
判据(这句已经写在 tools/news_source_atlas.yaml 里,是同一条):
这条源关掉之后,我们那篇稿还写得出来吗?写不出来 = 我们在拿它当供稿方,那就得有授权。
⚠️ 这一条必须落成代码,不能只写在提示词里 —— 提示词挡不住,代码才挡得住。
第三道 · 分工 —— 两道之间不许混
第一道只负责「知道发生了这件事」,第二道才负责「写出来」。 第一道读到的东西不是稿子的原料,是线索。
为什么这条最要紧: 外面那些 AI 公司现在挨的官司,几乎全是版权官司,没有一宗是因为不理 robots。 也就是说 —— 第一道怎么读都不是要害,要害在第二道有没有搬抄。
一眼看完
这套东西怎么切的
填不出来填 null,绝不编
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 都做了(还翻车了)。
在这两条:
- 读完能立刻吵起来。 今日头条要你看完再去别处聊;我们的新闻本身就在聊天软件里 —— 一条金价新闻,长按甩进群、直接开投票「买不买」。别人要复制这个,得先有一个 IM。
- 订阅,不是喂你。 头条靠推荐算法猜你想看,越猜越窄;我们是你自己挑类别,每天固定几条。 在信息过载的今天,「少而准」反而是卖点。
⚠️ 一句实话:广度上打不过今日头条,别往那儿比。 我们赢在小、准、能社交。 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。
这对我们的三条启示(这是本节最重要的部分):
- 「单独一个新闻 App」是个坏生意 —— 用户手机里已经有 Apple News / Google News, 再装一个次要新闻 app 的动力很弱。⚠️ 而我们不是要做单独的 app,我们是长在 HOOP 里的一个小程序 —— 这恰好绕开了 Artifact 的死因。这是我们和它最大的结构性差别,不是运气。
- 他们的技术值钱,产品不值钱 —— Yahoo 买的是推荐引擎,不是那个 app。 说明「AI 处理新闻」这件事本身有价值,但要挂在一个已经有用户的地方。
- 留存比下载重要得多。 发布那几天冲一波然后掉光,是这类产品的标准死法。
来源: - 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 称和解不代表承认过错)。
⚠️ 这个案子的三条,每条都能直接变成设计约束:
- 「5000 万月活」证明这条赛道商业上做得大 —— 方向没问题,问题在做法。
- 它虚构的不是观点,是「事件本身」 —— 一场根本没发生的枪击。 这正是 §3.11 Evidence Pack 存在的理由:先证明事件存在,再写它。 闸门查得再细,若一条新闻从头到尾只有一个来源,查的也只是「改写得像不像」。
- 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 才是有风险的一步):
robots.txt是硬约束,不是建议。 今日头条自己就公开写明媒体可以用 robots.txt 拒绝 ToutiaoSpider 收录。 我们的 Spider 必须有名有姓(带 UA 和一个说明页)、遵守 disallow、限速。⚠️ 这一条的「有名有姓」「遵守 disallow」两半已被 §0.0(08-31,Jeff 亲定「robots 是礼貌不是法律」)和 09-01 的身份决定(
spider_engine.pyIDENTITY 默认 browser,「代表真人去浏览」)作废 —— 现在只守限速(Crawl-delay 自动读、只放宽不收紧)。留在这里是为了让后来人知道曾经这么定过、以及为什么改;别照这一行写代码。 判据:一家媒体想拒绝我们,要有一个不用找律师就能做到的办法。- 抓回来的全文只进「证据库」,永不直接进发布。 全文用来核对事实(闸门 2/3/7 要拿它 grep),不用来生成句子。 这条是 §1.7 那 175 万和解的直接对策。
- 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
}
⚠️ 三条,其中第一条是我和原方案不同的地方:
confidence不用0.98这种小数,用「档」+ 可数的事实。 Jeff 08-13 刚让我把importance从8.7/10改成 1-5,理由是模型给不出稳定的 8.6 vs 8.7。0.98是同一个毛病,而且更危险 —— 它看起来像概率,会被当成概率去卡阈值。 改成:high / medium / low,由可数的事实推出来(几个独立来源、有没有一手件、有没有冲突), 不是模型自评(和 §3.0 硬规矩 ③ 同一个道理)。claims要一条条挂证据,不能整包挂。 「交易金额」和「是否已完成」可能一个有一手件、 一个只有传闻 —— 整包一个 confidence 会把弱的那条也镀成金。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。
我同意这是这套东西真正值钱的地方 —— 但要说两句实话:
- 它是第二个产品,不是这个产品的 P0。 Jeff 自己 08-13 刚定了顺序:「先做 Renderer,视觉不成立后面全白搭」。 那条仍然对。 Event Graph 别在 P0-A 点头之前开工。
- 但地基现在就别挖歪 —— 有两个决定是「以后想改要重来」的,现在就得做对:
- 发布单位是「事件」不是「文章」(§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_delay30 天缓存、每主机每班 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 · 没有合法图片来源的事件。
⚠️ 两条我要补在这上面:
- 体育结果放 GREEN 要小心 —— 赛果本身是事实,但赛果数据常常是有主的(体育数据商授权)。 Jeff 自己写了「在数据和图片权利明确的前提下」——那个前提要落成一道检查,不能只是备注。
- 「跑三个月再开 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.32,英文 1.25(#13 #14 #15 三张对比出来的)。
- 数字用等宽数字(
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)。绿箭头 = 外键指向,虚线框 = 外部表。
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 是必须的,不是可选的 —— 没有它,「每条都有出处」就是一句口号。
每天那一跑
7 道闸门
任何一项不过 → blocked,当天这条不发。⚠️ 这几关必须是代码,不是提示词。
news_article_sources 至少一行,且链接可达fact_level 与源一致;源写「洽谈/据报道」,稿子里不许出现「已收购/达成」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,不靠「不在名单里」 |
三条设计判据,改这一块之前先读:
- 那个内部标记是进程内的私有类型,不是 header / 参数 / cookie —— 外面的请求写不进来,所以伪造不了。判据:能被外面的人写的东西不能当闸。
- 缺省方向是关的(fail closed):ctx 里没有标记 = 不给待审稿。 猜错时代价往哪边倒 —— 猜「不给」最多是页面少几条,猜「给」是没审的东西上了公网。
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 查。
三条设计判据:
- 「有没有资格审」和「不能自审」是两条规矩,不是一条。 只比作者,会让记者 A 审记者 B,也会让任何普通房成员写结论 —— 那是扩权,不是简化(Gode 2026-09-12)。
- 作者永远由服务端按当前登录身份写,绝不收请求里自报的。 能自报,「不能自审」一秒被绕过去:交稿时把作者写成别人,回头用自己的号来审。
- 作者不明 = 不放行。 老稿是在
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 归别的账号,他装不了)——
所以我那句「各自能起各自的」在他机器上根本不成立。
洞还是长在交界上:我一次都没问过「你那台有什么」。 现在:
POSTGRES_DSN—— 你自己指(仍然只认本机);- 本机
127.0.0.1:5432连得上 → 用它; - 都没有 → 起一份内嵌 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 走,有后续就推 |
❌ 放详情页 |
⚠️ 三条设计纪律(不写清楚会出事):
- Ask AI 的上下文只有「这条 doc + 它的源文本」 —— 不许它带着整个互联网的记忆来答。 否则用户问「这会不会威胁 Nvidia」,它会拿训练里的旧事实回答,而那些没过我们的闸门。
- AI 的回答必须视觉上和新闻正文区分开,并标「这是 AI 的看法,不是报道」。 Apple 那次翻车(§1.2)就是用户分不清哪句是新闻、哪句是 AI 生成的。
- 群里问的,群里能看见 —— 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 |
⚠️ 三个必须看懂的点:
- 文本成本低到可以忽略。 就算全用 Opus 5,一个月也就 30 美金上下。 真正的成本变量是「类别数 × 每天跑几次」,不是模型档次。 加到 10 类、一天跑 2 次 → 直接 ×5。
- 成本和用户数无关 —— 因为稿件是共享的(§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)。 所以:
- 只写「决定」,不写「填空」。 这里只放改起来要重来的部分:数据契约、全局限速、 闸门判法、模板挑选。CRUD、路由、错误处理这些照着写就行的,不进图纸。
- 已经有真代码的,只放指针,不复制。 例如 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 问「还缺什么」,这是答案)
- 🔴 到今天为止,机器一条真新闻都还没发现过。 所有信心建立在纸上和 30 张手写数据卡上。 B3 那一步(真事件 + Evidence Pack)才是这个项目第一个真正的里程碑。
- §2.2 那些质量指标,现在一条都测不了。 「事实锚定率 ≥ 95%」是猜的 —— 等 B3 跑起来才有资格定。在那之前它是占位,不是标准。
- 有「下架」,没有「怎么知道该下架」。 用户举报入口在哪?谁值班?半夜谁看? —— 这是出事时才会发现的洞,已列为 D4,而且要求演练过一次才算完成。
- Ask AI 的成本和滥用没算。 十个人对同一条各问五遍 = 五十次调用。 需要:同一条新闻的问答共享缓存 + 每人每小时上限。(D3 的前置)
- 没有「我们不做什么」的清单。 图纸只写要做的 —— 不写不做的,以后每个人都会往里加。(见 §7,但那份太短)
- 成本只有文字那块是实的。 终局量级我写的是「我没有数字」—— 那不是完善,是诚实的空白,要等 §6 第 15 条(一天处理多少条)拍板才能算。
9.5 一句话总结这张表
🟢 现在能做的只有 A 队四步(视觉)+ B 队本地那半截;🔴 剩下 14 步全部等平台。 所以「等平台」不是一句托辞 —— 它挡住的是这个项目 70% 的工作量。
📊 走到哪了(这一段是生成的,不是手写的)
一共 142 把刀,已完成 14 把。 更新于 2026-08-31。
| 队 | 这一队在干什么 | 几把刀 | 已完成 |
|---|---|---|---|
| A | 🎨 A 队 · Renderer 与视觉(不碰数据库,平台挡不住) | 38 | 0 ▱▱▱▱▱▱▱▱▱▱ |
| B | 🕷️ B 队 · Discovery(抓取+解析,落文件不落库) | 37 | 2 ▰▱▱▱▱▱▱▱▱▱ |
| C | 🚨 C 队 · 闸门与契约(纯函数,现在就能写+测) | 21 | 0 ▱▱▱▱▱▱▱▱▱▱ |
| D | 🗄️ D 队 · 落库与上线(全部等平台) | 29 | 11 ▰▰▰▰▱▱▱▱▱▱ |
| E | 🧪 E · 验收、运营与善后 | 17 | 1 ▰▱▱▱▱▱▱▱▱▱ |
✅ 已经做完的,每条都带提交号
| 刀 | 做了什么 | 怎么做的 | 证据 | 提交 |
|---|---|---|---|---|
| D-29 | 26 家名单真灌进生产库(桌子建好了,东西也放上去了) | 迁移 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,数仍 26 | f52a68b9 |
| E-17 | 逐刀验收页 —— 他自己点得进去看每一刀走到哪 | 进度是算出来的(已完成几刀直接数);每条带提交号+证据;写早了/搁置的照实列;唯一出处 news_progress.yaml | https://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-01 | Source 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) | SG | media | robots 没禁 | 10 | 10 秒 | 29 | 不许 |
| The Straits Times | SG | media | robots 没禁 | 10 | 10 秒 | 86 | 不许 |
| The Business Times (Singapore) | SG | media | robots 没禁 | 10 | 10 秒 | 1 | 不许 |
| Bank Negara Malaysia | MY | primary | robots 没禁 | — | 5 秒 | 1 | 不许 |
| Securities Commission Malaysia | MY | primary | robots 没禁 | — | 5 秒 | 1 | 不许 |
| Department of Statistics Malaysia | MY | primary | robots 没禁 | — | 5 秒 | 1 | 不许 |
| Ministry of Finance Malaysia | MY | primary | robots 没禁 | — | 5 秒 | 14 | 不许 |
| Ministry of Investment, Trade and Industry Malaysia | MY | primary | robots 没禁 | — | 5 秒 | 0 | 不许 |
| Prime Minister's Office of Malaysia | MY | primary | robots 没禁 | — | 5 秒 | 1 | 不许 |
| Singapore Exchange | SG | primary | robots 没禁 | — | 5 秒 | 0 | 不许 |
| Ministry of Trade and Industry Singapore | SG | primary | robots 没禁 | — | 5 秒 | 1 | 不许 |
| London Bullion Market Association | GB | primary | robots 没禁 | — | 5 秒 | 1 | 不许 |
| World Gold Council | GB | primary | robots 没禁 | — | 5 秒 | 33 | 不许 |
| Associated Press | US | wire | robots 没禁 | — | 5 秒 | 18 | 不许 |
| The Edge Malaysia | MY | media | robots 没禁 | — | 5 秒 | 0 | 不许 |
| The Star | MY | media | robots 没禁 | — | 5 秒 | 8 | 不许 |
| New Straits Times | MY | media | robots 没禁 | — | 5 秒 | 1 | 不许 |
| Astro Awani | MY | media | robots 没禁 | — | 5 秒 | 16 | 不许 |
| Free Malaysia Today | MY | media | robots 没禁 | — | 5 秒 | 12 | 不许 |
| Malay Mail | MY | media | robots 没禁 | — | 5 秒 | 16 | 不许 |
| Monetary Authority of Singapore | SG | primary | robots 没禁 | 2 | 2 秒 | 0 | 不许 |
| Singapore Department of Statistics | SG | primary | robots 没禁 | 2 | 2 秒 | 1 | 不许 |
| Bernama(马来西亚国家通讯社) | MY | wire | robots 没禁 | 1 | 1 秒 | 2 | 不许 |
| Reuters | GB | wire | robots 说不许 | — | 5 秒 | 1 | 不许 |
| Bursa Malaysia | MY | primary | 未知(按不许办) | — | 5 秒 | 0 | 不许 |
| CME Group (COMEX 黄金期货) | US | primary | 未知(按不许办) | — | 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 条
news_type、status、body_blocks、entities、schema_versionimportance 用 1-5 整数还是 8.7/10 一位小数/bot 那一页挂在哪个域名hoopcomm.com/bot6. ⚠️ 要 Jeff 拍板的(我没有替你决定)
7. 这份图纸没做的事(边界)
- ❌ 小程序平台的任何设计 —— 归 Fable(Jeff 2026-08-13 明确)
- ❌ 任何代码、迁移文件、迁移号
- ❌ 部署与发版
写于 2026-08-13 · Vega(平台架构/接缝线) 下一步:等 Jeff 看完 §6 那几条,拍完板再谈动手。