架构
上级:lucerna · 仓库根目录:
~/Develop/projects/Otium/
以谁为准代码才是权威;
ops/ARCHITECTURE.md(2026-05-23)在几乎每一条基础设施陈述上都已经过时——以下全部对照代码逐条确认过:
- 桌面端用的是 Electron,不是"Tauri 2"(
desktop/package.json里是 electron 33 +electron-builder+better-sqlite3;main/*里import from 'electron')。文档撰写之后发生过一次 Tauri→Electron 的迁移;桌面端代码里大量wa-sqlite/Tauri 相关注释都是迁移前遗留的复制粘贴。- 桌面端的本地存储是通过 IPC 使用原生
better-sqlite3,不是 wa-sqlite(文档里说的"wa-sqlite cache"只对网页端成立)。schema 块是每次启动都重新执行一遍的FULL_SCHEMA_SQL;PRAGMA user_version迁移框架只在网页端还在用。- Bearer-token 鉴权在桌面端已经上线,不是"计划中"(
setSessionProvider、Authorization: Bearer)。- 仍然停留在设想阶段的:
library混合分片(配额/本地迁移)、真正的按用户隔离(桌面端每一行数据目前都用字面量'local'这个用户,切换会话就整体清空)、以及宣传中提到的browser-plugin/mobile端(并不存在)。landing/是一个空的、已过时的克隆;真正的官网是LandingPage/(Payload CMS)。身份系统处于双轨/过渡状态(Appwrite 服务和 better-auth 的表都同时存在)。
技术栈(已核实)
- 领域核心: 框架无关的 TS(
@lucerna/core,唯一的 peer 依赖是zustand,用tsup构建)。 - 共享 UI:
@lucerna/design-system(React 19、Radix、CVA、Tailwind 4)。 - 外壳(shell): Next.js 15(网页端)· Electron + Vite(桌面端)· Next.js 15 + Payload CMS 3(官网)。
- 后端: Go 1.26 + echo(
auth基于 Appwrite;mainbackend是领域 API,用pgx+google/wire,负责 SRS)· Python + FastAPI + spaCy(nlp-api,德语+英语)。 - 数据: Postgres;客户端 SQLite(网页端用
wa-sqlite,桌面端用better-sqlite3);flexsearch。各仓库靠私有的 Verdaccio registry 联系在一起;用 Coolify 部署;静态资源放在 R2。
阅读管线(主干)
摄入(ingest)100% 在客户端完成;服务端只存解析后的结果。撑起整个设计的核心想法是以内容为键、双坐标的句子。
- 解析(
core/src/upload/parseEpub|parsePdf|parseText):EPUB 经 JSZip → OPF/spine → 拼接去掉 HTML 标签的各章节;目录从 NCX/EPUB3-nav 读取,带累计字符偏移量;拒绝扫描件/过薄的书。 contentHash(upload/contentHash.ts,MD5,特意保持与旧版兼容)是关联键——词汇、笔记、句子缓存全部锚定在它上面,所以重新上传同一本书也能重新关联上。CachedSentence(持久化单元,键为(contentHash, sentenceIdx))携带两套坐标系统——原始来源偏移量,以及用户实际读到的显示偏移量——外加一个displayToRaw[]映射表。这是整个系统的脊梁,详见 raw-display-cursor。Page是一个瞬态的运行时聚合(内存里只保留当前这一页);由排版逻辑决定哪些句子共享同一个pageNumber——详见 dynamic-pagination(基于 DOM 实测,随字号/视口变化重新排版)。- 在同一套句子流之上:full-text-search(按书隔离的 FlexSearch,落盘持久化),以及桌面端的 tts-audiobook(本地 Piper 有声书,卡拉OK式高亮锚定在原始偏移量上)。为这一切提供输入的摄入流程见 upload-ingestion。
阅读领域模型(大白话版)
Book(全文 + ISBN 溯源信息)→ 切分成若干 CachedSentence → 逐页阅读(Page,瞬态)→ 位置/标记的存储方式是锚定内容,而非锚定几何位置,这样字号/排版变化时它们依然有效:Anchor(当前阅读位置 = 不超过 200 字符的句子前缀)、Bookmark、ReadingNote(带有全书绝对原始偏移量的高亮)、VocabularyEntry(词元 + 全部屈折形式 forms[] + 词性/VerbTable/NounInfo + context + SRS 状态)。POSCategory(十分类词性体系、颜色、11 种语言的标签、别名归并)和 VerbTable(变位表)是其中的语言学值对象(value object)。
多仓库形态
没有用 monorepo 工具——约 12 个兄弟仓库靠 Verdaccio registry 彼此衔接。
| 仓库 | 角色 | 技术栈 |
|---|---|---|
core | @lucerna/core 领域核心 | TS, tsup |
design-system | 共享 UI 原子组件 + tokens | TS/React 19 |
web | 网页端外壳(在线优先 + 读取缓存) | Next.js 15, wa-sqlite |
desktop | 桌面端外壳(离线优先) | Electron, better-sqlite3, sherpa-onnx |
LandingPage | 官网 | Next.js 15 + Payload CMS 3 |
auth | 身份(基于 Appwrite) | Go + echo |
mainbackend | 领域 API,负责 SRS | Go + echo + pgx + wire |
nlp-api | spaCy/依存句法/翻译/OCR | Python + FastAPI |
e2e · ops · blog · landing(空) | 测试 · 基础设施 · 内容 · — | — |