Skip to content

我为什么把古籍网站拆成 Studio 和 Reader:ReadTao 的整体架构

约 2537 字大约 8 分钟

ReadTaoVueFastAPI系统架构

2026-06-24

ReadTao 不是把几份古籍 TXT 放到网页上展示,而是把来源、整理、审校、发布和阅读拆成一条可以长期维护的数据链。这篇先从整体架构出发,记录我为什么把项目分成 Content Studio、Reader、Backend 和 Worker,以及这些模块之间最重要的边界。

最近把 ReadTao 的第一阶段代码整理并上传到了 GitHub,也终于可以回头系统地写一遍这个项目。

ReadTao 是一个古籍阅读网站。它现在的定位很克制:我会按照自己的时间和兴趣逐部整理典籍,尽量把来源、底本、原文、译文、注文和发布版本说明白,再给读者一个适合桌面和手机的阅读界面。

项目里已经有拼音、翻译、Embedding 和“问道”问答,但这些都不是网站成立的前提。即使关掉全部 AI Provider,已经发布的典籍仍然应该可以阅读、导出、备份和恢复。

这件事看起来只是一个产品边界,真正落到代码里,却决定了我为什么不能只做一个前台页面加几张内容表。

这一组文章会沿着同一条内容链展开

原始文件怎样进入 Studio,怎样完成来源分层、语义分段、派生内容和审校,怎样生成不可变发布快照,最后又怎样被 Reader 稳定读取。

ReadTao Reader 与 Content Studio 演示

完整代码和本地演示说明都在 ReadTao GitHub 仓库 中。

最开始遇到的不是页面问题

如果目标只是展示一篇固定古文,Markdown、静态站点甚至一份 JSON 都能完成。

但我真正开始整理古籍以后,很快遇到了另一组问题:

  • 同一部典籍可能存在不同底本,不能把某一个来源写成“唯一标准版”;
  • 原始文件中的正文、注文、卷题和其他材料不能混为一谈;
  • 断句和语义段落需要人工确认,不能只按字符数机械切片;
  • 原文、简体、拼音、现代汉语和英文会分别修订;
  • AI 生成的是初稿,不能直接获得发布权限;
  • Studio 里正在修改的草稿,不能立即改变读者已经打开的页面;
  • 一个公开段落需要有稳定地址,也要能够回到具体来源和发布版本。

这些问题的共同点是:它们关心的不是“现在显示哪一段文字”,而是“这段文字从哪里来,经过了什么过程,当前公开的是哪一个版本”。

因此,ReadTao 的核心不是一个页面,而是一条内容生命周期。

为什么拆成 Studio 和 Reader

我最终把面向编辑者和面向读者的界面完全拆开。

Content Studio 处理“还在变化的内容”

Studio 是一个桌面内容工作台,负责:

  • 登记作品、底本、来源和许可信息;
  • 上传并保留原始文件;
  • 确认正文、来源注文、非正文资料和待界定片段;
  • 拆分或合并语义段落;
  • 生成简体、拼音、现代汉语和英文初稿;
  • 保存修订、提交审校、批准或驳回;
  • 检查完整性并发布整部底本;
  • 查看异步任务、失败原因、发布历史和审计记录。

这里的数据天然是不稳定的。编辑者可能正在修改原文,也可能刚驳回一条译文,某个生成任务还可能只完成了一半。

Reader 只处理“已经公开的内容”

Reader 面向普通读者,只关心当前已经发布的版本:

  • 首页和书库;
  • 章节目录与稳定段落链接;
  • 原文、拼音、来源注文和译文;
  • 对照、仅原文、仅译文等阅读方式;
  • 桌面左右布局与移动单列布局;
  • 阅读偏好、继续阅读、Markdown/TXT 导出和打印。

Reader 不读取草稿表,也不关心某次生成任务进行到了多少。它从 edition.current_release_id 指向的发布快照构造只读内容。

这道边界给项目带来的好处很直接:Studio 可以继续工作,Reader 仍然稳定;下一次发布失败,线上继续读取上一个版本。

四个运行模块分别负责什么

项目目前采用 pnpm 与 uv 管理的单仓库,没有一开始就拆成多个独立仓库或微服务。

模块当前技术主要职责
ReaderNuxt 4、Vue 3响应式阅读、稳定路由、本地偏好、导出与打印
Content StudioVue 3、Vite导入、分层、分段、生成、修订、审校和发布
Application APIFastAPI、SQLAlchemy、Alembic认证、内容规则、发布事务和公共读取投影
Content WorkerDramatiq、Redis拼音、翻译、Embedding 等可重试异步任务
Data LayerPostgreSQL、pgvector、MinIO业务内容、向量和不可变原文件

我没有把 Backend 再拆成内容服务、发布服务和用户服务。当前体量下,一个边界清楚的单体更容易部署、迁移和恢复。真正需要隔离的是数据责任,而不是进程数量。

apps/
  reader/              # Nuxt Reader
  studio/              # Vue/Vite Content Studio
  backend/             # FastAPI、Worker、Alembic
packages/
  api-client/          # OpenAPI 快照与生成的 TypeScript 类型
tests/
  e2e/                 # Reader/Studio Playwright 冒烟
docs/                  # 产品、架构、数据和交付记录

Backend 是两端之间的规则中心

Studio 和 Reader 都访问同一个 FastAPI 应用,但使用不同的接口边界。

Studio 侧是需要登录和权限校验的管理命令,例如:

POST /api/studio/imports
GET  /api/studio/divisions/{division_id}/workspace
POST /api/studio/divisions/{division_id}/jobs
POST /api/studio/editions/{edition_id}/releases

Reader 侧则是公开只读接口:

GET /api/public/works
GET /api/public/works/{work_slug}
GET /api/public/works/{work_slug}/editions/{edition_slug}/chapters/{division_slug}
GET /api/public/works/{work_slug}/editions/{edition_slug}/content

这些接口不只是 CRUD。真正重要的规则,例如来源覆盖必须连续、派生内容必须基于最新原文、整部内容单元必须齐全、发布必须在一个事务里完成,都由 Backend 再次检查。

前端可以提前给出友好提示,但不能成为业务不变量的唯一守门人。

Worker 为什么不能直接发布

翻译、拼音风险复核和向量化都可能耗时,也会受外部 Provider 影响,所以它们被放进异步任务。

但 Worker 的权限被刻意限制在“产生机器初稿或派生数据”:

即使模型返回了一份看起来很完整的译文,任务成功也只代表“初稿已经生成”,不代表内容已经通过审校。

这让我可以在以后替换模型、停用 Provider,甚至完全改成人工录入,而不破坏发布和阅读链路。

OpenAPI 不是附带文档,而是共享契约

Reader 和 Studio 都依赖 Backend 返回的结构。如果在两个前端分别手写一套 TypeScript 类型,很快就会出现字段已经变更、页面仍按旧结构读取的问题。

ReadTao 直接从 FastAPI OpenAPI 生成共享类型:

pnpm api:generate
pnpm api:check

生成结果放在 packages/api-client/,Reader 和 Studio 通过 workspace 依赖使用它。api:check 会重新导出契约,再检查生成文件是否出现未提交差异。

它不能代替集成测试,却能尽早发现“后端改了接口,前端类型还停在过去”的问题。

本地运行时,我希望入口尽量统一

package.json 不是第三个前端应用,而是整个仓库的命令入口。

pnpm install
uv sync --project apps/backend
pnpm infra:up
pnpm db:upgrade
pnpm demo:seed

随后分别启动:

pnpm dev:backend
pnpm dev:reader
pnpm dev:studio

预置演示使用仓库中的王弼《道德真經注》来源文件,不调用外部模型。它适合查看 Studio 到 Reader 的完整结果;需要继续生成或验证异步流程时,再启动 pnpm dev:worker

关于演示内容

仓库中的 KR5c0073 是一个有明确来源的工程样例,不是所谓“唯一标准《道德经》”。演示中的现代汉语和英文也是本地占位初稿,不能当作正式译注传播。

我现在怎样理解 ReadTao 的架构

做完第一条完整链路以后,我对这个项目的理解已经从“做一个古籍阅读器”变成了:

Studio 负责让内容变得可信,Release 负责让公开版本稳定,Reader 负责让可信内容真正可读。

这三件事必须连接起来,也必须保持边界。

后面的四篇会继续沿着这条链深入:

  1. 古籍不是上传一个 TXT 就结束了:Content Studio 的导入与分段设计
  2. AI 只能写初稿:内容生成、人工审校与失败重试
  3. Studio 改了内容,Reader 为什么不会立即变化:不可变发布设计
  4. 一个古籍阅读器不只是排版:Reader 的响应式设计与状态模型

这一篇先把地图铺开。下一篇从最靠近原始资料的地方开始:一份古籍 TXT 进入 Studio 以后,到底还要经历什么。