ai-ml、mental-models、system-design、cs-papers-deepread……每天(或隔天/每周)新增一页深度内容,中文页 + 英文页各一个独立 HTML。/ 呼出)、Giscus 评论区、图片 lightbox、双语切换 + TTS 朗读——全部由共享 JS 提供,内容页自己一行都不用写。核心思路一句话:我只维护每个站的路线图(TOPICS.md),内容生产、发布、加工、聚合、巡检全部交给自动化——其中「写内容」这一步交给云端定时运行的 Claude agent。
┌─ 人(我)──────────────────────────────┐
│ 维护 TOPICS.md 路线图 · 定封顶 · 审月度建议 │
└──────────────┬────────────────────────┘
▼
┌── 生成层:claude.ai 云端 routine(20 个定时 trigger)──┐
│ cron 错峰触发 → 挂载对应 repo → 按 CLAUDE.md 写作规范 │
│ 选题(文件系统幂等) → 写 zh+en 页 → 更新索引 → publish.sh │
└──────────────┬────────────────────────────────────┘
▼ git push
┌── 仓库层:20+ 内容 repo(GitHub Pages)───────────────┐
│ TOPICS.md · CLAUDE.md · .maxchars · publish.sh 校验闸门 │
└──────────────┬────────────────────────────────────┘
▼ push 触发
┌── 加工层:每仓 GitHub Actions ────────────────────────┐
│ 注入共享 JS · (mental-models) Azure TTS bake │
└──────────────┬────────────────────────────────────┘
▼ 每日定时
┌── 聚合层:hub 仓 GitHub Actions ──────────────────────┐
│ refresh-hub.yml 重渲染首页 · build-search.yml 搜索索引 │
└──────────────┬────────────────────────────────────┘
▼
┌── 治理层 ────────────────────────────────────────┐
│ 封顶自动停机 + 中文泄漏检测 · 云端月度前沿刷新 │
└────────────────────────────────────────────────┘
分层的原则是:每一层只信任下一层的产物,不信任它的过程。生成层可能写崩,所以仓库层有校验闸门;校验闸门可能漏,所以治理层有巡检。
每个内容 repo 除了 HTML 页面,只有四样东西,各司其职。
整个系统里唯一持续需要我投入的地方。它按顺序列出这个站要覆盖的全部主题,好几个站还写明了封顶编号(比如思维模型封顶 68 期)。关键设计是权限单向:routine 只能读它,publish.sh 会直接拒绝任何修改了 TOPICS.md 的提交。选题耗尽时,routine 不许自己续写路线图,只能给我发一条推送通知请求补充。
这条线划在这里,是整个系统「不跑偏」的根本:AI 决定怎么写,人决定写什么。
需要精确控制版式和深度的站(system-design、论文精读、每日精读)有一份详细的执行规范:目标读者(资深工程师)、篇幅区间、必备章节结构(比如论文精读的「一句话 → Glossary → 坐标 → 问题动机 → 核心思想 → 关键结果 → 影响 → 局限与批评 → 要点收尾」九段式)、配色(每个站有独立的视觉签名:system-design 是青色暗色系,论文精读是琥珀铜色)、以及诚实性要求——引文拿不准要标「大意」,局限和反方观点必须写。
规范里最值得一提的是以已发布页面为基准:prompt 会指着某一篇(「以 read1 为深度、格式与腔调基准」)说照这个来。比起在规范里描述风格,直接给范例样本要稳定得多。
一个只有一行数字的文件(3500/4000/5000),publish.sh 会统计新页面的 CJK 字符数并卡在这个上限。这是踩过坑之后加的:LLM 生成内容有「长度棘轮」倾向——每篇都比上一篇略长一点,因为模型倾向参考最近的页面,几十天后页面就膨胀失控。解法是双向夹住:prompt 里给目标区间,闸门上给硬上限。
所有 repo 共用同一个 150 行的 bash 脚本,routine 写完页面后必须通过它发布。它做的检查:
.maxchars(防膨胀)<div> 开闭平衡(防截断的 HTML——LLM 输出被截断是真实会发生的)git add/commit/push,commit message 统一为 Add #N: 标题最后这个格式约定不是小事——commit message 本身成了机器可读的发布记录,下游的完结检测、hub 徽章都靠解析它工作。
内容生产由 claude.ai 的云端定时任务(scheduled agent / routine)完成,目前有 20 个 trigger。每个 trigger 的 job 结构:
Bash / Read / Write / Edit / Glob / Grep / WebSearch / WebFetch / PushNotification,够用即止以「每日精读」为例,它的 prompt 就五步:
ls *-read*.html。文件系统就是数据库:不需要任何状态存储,重复触发天然幂等,因为下一次运行看到的文件列表已经变了。{slug}-read{N}.html + {slug}-read{N}.en.html,两版都要求地道而非直译,互相有语言切换链接;同时更新两个语言的索引页。./publish.sh,过闸即上线。Prompt 最后一句是「务必自主完成、不等任何确认」——云端 routine 没有人在场,任何一步等确认就是死锁。
除了 20 个内容 routine,还有两个元 routine:
ROADMAP-SUGGESTIONS.md——只建议,绝不直接改任何 TOPICS.md。经典领域(哲学、佛学、数学)默认「本月无新增」。建议也必须先搜索验证来源真实存在,宁缺勿滥。routine push 完就下班了,但页面还没到最终形态。每个 repo 的 GitHub Actions 接手做两类后处理。
评论区、搜索、双语 TTS、导航按钮、lightbox 这五个能力由 hub 仓托管的共享脚本提供(comments.js、search.js、i18n-tts.js、index-button.js、lightbox.js)。内容页不允许硬编码这些 script 标签(publish.sh 会拦),而是 push 后由注入 Action 扫描 HTML、缺哪个补哪个,再以 Auto-inject shared scripts 自动提交。
为什么注入而不是让 routine 直接写进模板?因为基础设施要能独立于内容演进。20 多个仓、几百个页面,如果 script 标签写死在生成模板里,升级一个搜索脚本就要重训 20 个 prompt、重刷几百页。注入制下,共享脚本改一处,全站下一次注入即生效;旧页面也能追溯补齐。
思维模型站的每一节都能点击朗读,音频是预先「烘焙」好的:
bake-tts.yml → 跑 bake-tts.py<h2> 分节提取正文 → 对文本取 hash → 查 audio/zh/<hash>.mp3 是否已存在 → 不存在才调 Azure Speech REST API 合成data-tts-zh="<hash>" 写回 HTML 元素,前端 i18n-tts.js 按属性找音频播放;没有预烘焙音频的站自动降级到浏览器 Web Speech API[skip bake] 标记,防止「提交触发 bake、bake 又产生提交」的死循环hash 寻址让整条链路幂等:内容没变就不花一分钱 API 配额,改了一节只重烘那一节。这个站目前积累了 500 多个 mp3、约 1.2 GB——全部躺在 git 仓库里由 Pages 直接伺服,零额外存储成本。
(这条 TTS 链路早期用的是火山引擎,后来整体迁到 Azure,旧脚本还留着 .bak 当化石。)
Hub 仓自己也是全自动的,两个每日 Action。
跑 generate_hub.py。这个脚本是典型的「单一数据源渲染双语」:卡片元数据(标题、双语简介、配色、分区)是脚本里一个 CARDS 数组,中文页和英文页从同一数组渲染,不存在两份要同步的 HTML。
动态部分靠 GitHub REST API:
-dayN/-weekN/-readN.html),把日期渲染到卡片上——所以徽章日期反映的是「最近更新了内容」,而不是被 bot 的注入提交污染的「最近有 commit」。Add #N 的最大值,达到封顶就把日期换成「✓ 已完结」徽章。这就是前面说的 commit message 格式约定的下游收益——完结状态不需要任何人标记,每个站自己「宣布」毕业。时间上,这个 Action 排在所有内容 routine 之后约 45 分钟跑,保证当天新页面能反映到首页。
静态站没有后端,全站搜索用 Pagefind:每天把所有内容仓 clone 到 _src/ 聚合成一棵树,跑 Pagefind 生成分片索引(中英文分开索引),发布到 hub 仓的 /pagefind/。前端 search.js 提供一个悬浮搜索按钮,按页面语言弹相应语言的搜索层。索引时排除页脚、导航、评论容器这些噪音区块。
有一个特殊处理:思想家圆桌辩论站(thinker-arena)是客户端渲染的(内容在 JSON 里),爬不到文本。解法是 render_search_snapshots.py 在建索引前把 JSON 辩论渲染成纯 HTML 快照专供 Pagefind 消费——给爬虫做一份 SSR,只不过是每日批处理版。
跑起来只是开始,长期无人值守才是难的部分。几道防线:
每个站的 roadmap 是有限的——学完就该毕业,而不是为了「日更」注水。封顶编号写进三处:TOPICS.md(routine 可见)、hub 的 CAPS 表(徽章)、以及一个本地定时巡检任务:每周核对每个封顶站实际发布的最高期数,写到顶的就通过 API 把对应云端 trigger 直接 enabled=false 停掉。
停机操作有两道防呆,都是踩坑换来的:
get 该 trigger,确认它挂载的仓确实是要停的那个仓——防止 trigger ID 对错位置,把别的站停了;{"enabled": false}。因为这个 API 的 update 对 job_config 是整体替换而非合并——带上不完整的 job_config 会把 prompt、挂载仓、模型配置整个洗掉。双语生成最常见的退化是中文漏进英文页的模板槽(副标题、标签、人名栏)。同一个巡检任务对全部仓的 *.en.html 跑一组指纹 grep(如 class="en">[一-鿿]),命中即报。指纹是精心挑的——佛经原文配英译、术语括注这类合法的中文不用这些 class,不会误报。
.maxchars 硬上限 + prompt 目标区间ls 的结果;发布记录 = commit message;完结状态 = 从 commit 解析。没有任何一处需要独立维护的状态存储,所以没有任何一处会和现实脱节。Add #N 的 commit 格式、{slug}-day{N}.html 的命名、.maxchars 单行文件——组件之间靠这些朴素约定通信,没有一处 JSON schema,但每处约定都有至少两个消费者。一个人的注意力是这个系统里最贵的资源。整条 pipeline 的设计目标从来不是「全自动」本身,而是把我的注意力从生产和运维里解放出来,全部花在唯一值得花的地方:决定接下来学什么。