2026-09-23·by Sijie Wang#node#lucerna#software#architecture

architecture

架构

上级:lucerna · 仓库根目录:~/Develop/projects/Otium/

以谁为准

代码才是权威;ops/ARCHITECTURE.md(2026-05-23)在几乎每一条基础设施陈述上都已经过时——以下全部对照代码逐条确认过:

  • 桌面端用的是 Electron,不是"Tauri 2"desktop/package.json 里是 electron 33 + electron-builder + better-sqlite3main/*import from 'electron')。文档撰写之后发生过一次 Tauri→Electron 的迁移;桌面端代码里大量 wa-sqlite/Tauri 相关注释都是迁移前遗留的复制粘贴。
  • 桌面端的本地存储是通过 IPC 使用原生 better-sqlite3,不是 wa-sqlite(文档里说的"wa-sqlite cache"只对网页端成立)。schema 块是每次启动都重新执行一遍的 FULL_SCHEMA_SQLPRAGMA user_version 迁移框架只在网页端还在用。
  • Bearer-token 鉴权在桌面端已经上线,不是"计划中"setSessionProviderAuthorization: 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 读取,带累计字符偏移量;拒绝扫描件/过薄的书。
  • contentHashupload/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 字符的句子前缀)、BookmarkReadingNote(带有全书绝对原始偏移量的高亮)、VocabularyEntry(词元 + 全部屈折形式 forms[] + 词性/VerbTable/NounInfo + context + SRS 状态)。POSCategory(十分类词性体系、颜色、11 种语言的标签、别名归并)和 VerbTable(变位表)是其中的语言学值对象(value object)。

多仓库形态

没有用 monorepo 工具——约 12 个兄弟仓库靠 Verdaccio registry 彼此衔接。

仓库角色技术栈
core@lucerna/core 领域核心TS, tsup
design-system共享 UI 原子组件 + tokensTS/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,负责 SRSGo + echo + pgx + wire
nlp-apispaCy/依存句法/翻译/OCRPython + FastAPI
e2e · ops · blog · landing(空)测试 · 基础设施 · 内容 · —

关键设计(支柱)

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →

architecture