微光阅 Lumos Reader:把 NAS 书库带进随身阅读异世界

有些人把 NAS 当作家庭服务器,有些人把它当作影音仓库,而我更愿意把它看成一座安静的私人图书馆。
问题在于:书可以安稳地躺在硬盘里,但阅读不应该被绑在 NAS 旁边。每次打开一本书,都先把整本文件搬到手机上,多少有点像为了看一页漫画,先把整套单行本扛回家。
所以我做了 微光阅 Lumos Reader:把书留在 NAS,把阅读带在身边。
它是一个面向个人书库的开源阅读系统。服务端负责守着文件,Web 和 Android 客户端负责浏览与阅读;正文、封面、漫画页和字体都按需读取,读到哪一页,就取哪一点内容。

项目地址:github.com/Alumos/LumosReader
它解决的到底是什么问题?
如果你的书都在 NAS 上,那么通常会遇到几个很现实的问题:
- 手机端想看书,却不想先下载整个 EPUB、PDF 或 CBZ。
- 在 Web 和 Android 之间切换时,希望阅读进度、收藏和标签能保持一致。
- 小说、漫画、PDF 的阅读方式不同,不能用同一个简陋的文件列表糊弄过去。
- 书库是自己的,最好就一直留在自己的设备上,不必把整套书交给第三方云服务。
Lumos Reader 的目标并不是做一个“什么都能打开一点”的文件浏览器,而是把 NAS 变成一个真正可以长期使用的私人书库:服务端管理内容和状态,客户端只负责把阅读体验做好。
一句话看懂整体架构
只读 NAS 书库 │ ▼Go 服务端 ── SQLite:元数据 / 进度 / 收藏 / 标签 / 统计 │ ├── Web:React + Vite + foliate-js / PDF.js └── Android:Compose + Rust/UniFFI + 原生阅读组件这个结构的核心思路很简单:书籍内容尽量只读,阅读状态单独保存;文件按需传输,客户端各自发挥。
这样做的好处是,NAS 上的书库可以保持稳定,服务端也不需要为了某个客户端去复制整套文件。Web 端和 Android 端共享同一个 API,进度、收藏、标签和阅读时长自然也就能同步。
服务端:安静地守着书库
服务端使用 Go 编写,负责扫描书库、读取元数据、管理阅读状态,并向不同客户端提供统一 API。
目前支持的内容包括:
- EPUB、MOBI、AZW3、PDF、CBZ/ZIP 和 TXT。
- EPUB 标题、作者、系列、封面、固定版式和翻页方向等元数据。
- 阅读位置、locator、最近阅读时间、累计阅读时长、收藏和标签。
- 书架分类、搜索、字体上传与下载,以及近 30 日阅读统计。
这里比较重要的一点,是内容传输支持 HTTP Range,同时配合 ETag、Last-Modified 和缓存策略。对于 PDF、漫画和大体积电子书来说,这比“打开时把整本文件读进内存”要实际得多。
漫画也有单独的处理路径:服务端可以先返回页面目录,客户端只请求当前页附近的图片,不需要提前把整个 CBZ 解压到磁盘。翻页时拿到下一页,体验就会更接近真正的阅读器,而不是在文件管理器里逐张点图片。
Web 端:书架、阅读器和搜索都在一个入口
Web 客户端使用 React 和 Vite 构建,适合在电脑、平板以及手机浏览器中使用。
它包含一套完整的书库工作流:
- 最近阅读、书架、分类树、格式筛选、标签筛选和只看收藏。
- 网格与列表两种视图,方便在“看封面”和“快速扫描文件”之间切换。
- 多卷漫画在网格视图中合并为系列卡片,列表视图中保留单卷信息。
- 书籍详情页支持收藏和标签编辑,服务端返回的数据作为统一来源。
- EPUB、MOBI、AZW3 使用
foliate-js,PDF 使用PDF.js。 - 章节、卷册、字体和阅读设置都围绕阅读过程组织,而不是把功能堆在菜单里。
尤其是 PDF 和 CBZ 的读取策略,它们不会默认把整本内容一次性预取。对部署在家用 NAS 上的服务来说,这不仅能减少等待,也能降低带宽和内存压力。
Android 端:把书库装进口袋
Android 客户端通过同一个服务端地址连接书库,不需要为移动端再维护一份孤岛数据。
客户端使用 Jetpack Compose 构建界面,并通过 Rust + UniFFI 处理网络、Cookie、Range 请求、元数据和进度队列。阅读位置会保存更细的 locator,因此 EPUB 章节、PDF 页面、漫画页和文本内容都能尽量准确地恢复到上次离开的地方。
此外,Android 端还准备了几类适合长时间阅读的能力:
- 字体库与自定义字体。
- 字号、阅读主题和翻页动画。
- E-INK 黑白模式。
- 图书与漫画分区浏览。
- 收藏、标签和阅读统计同步。
简单说,NAS 负责“我有哪些书”,Android 负责“我现在读得舒不舒服”。两者各司其职,阅读体验才不会被基础设施拖后腿。
图书和漫画,不应该被一锅端
Lumos Reader 对书籍类型的判断不是只看文件后缀,而是综合书架设置、文件格式、EPUB 固定版式和路径信息:
- 书架被显式指定为图书或漫画时,以设置为准。
CBZ和ZIP默认视为漫画。- 固定版式 EPUB 默认归入漫画类内容,但仍然使用 EPUB 阅读器渲染。
- 路径中包含“漫画”、
manga、comic或コミック时,倾向于识别为漫画。 - 其他内容归入图书。
这里有一个容易被忽略的细节:分类信息和渲染器不是一回事。
一部固定版式 EPUB 可以被归到漫画书架,但真正打开时仍然走 EPUB 阅读器;客户端不会因为一个分类字段,就把所有内容粗暴地送进图片阅读器。看似只是一个判断顺序,实际却决定了书架是否会越用越乱。
推荐的目录结构如下:
/library/├── 漫画/│ └── 爱情/│ └── 某个系列/│ ├── 01.cbz│ └── 02.cbz└── 小说/ └── 爱情/ └── 某本小说.epub当然,如果你的目录结构比较自由,也可以在 Web 或 Android 端为书架显式指定目录和内容类型。
在 NAS 上部署
最推荐的方式是使用 Docker Compose。进入项目目录后,先准备环境文件:
cp .env.example .env然后编辑 .env,至少修改管理员密码、书库目录和状态目录,再启动服务:
docker compose pulldocker compose up -d默认服务只绑定到本机 127.0.0.1:8080。如果希望在 NAS 的其他端口访问,可以配置 PORT,然后通过反向代理提供 HTTPS。
部署时有几条建议值得认真对待:
- 书籍目录尽量保持只读挂载。
.env、书籍内容和数据库不要提交到 Git。- 公网使用时务必配置 HTTPS,并设置真正的强密码。
- 状态目录和字体目录单独备份。
- 不要把整本书或包含隐私的日志上传到 Issue。
这个项目不包含任何书籍内容,书库的版权和访问权限需要由部署者自行确认。工具可以帮助我们更舒服地阅读,但它不会替我们解决内容授权问题。
给喜欢自己折腾的人
如果你不想直接使用镜像,也可以本地开发。项目由 Go 服务端、React/Vite Web 端和 Android 客户端组成,分别位于:
cmd/lumosreader/ Go 程序入口internal/server/ HTTP API、扫描器、EPUB 元数据、SQLite、运行时web/ React/Vite 前端与嵌入资源clients/android/ Compose 应用、Rust/UniFFI、native 阅读组件scripts/ UI 与回归检查脚本docs/ 预览图和开发设计记录服务端最终可以把构建后的 Web 静态资源嵌入 Go 二进制,镜像运行时保持精简。对于 NAS 这类长期运行、希望少维护一点的设备来说,这种发布方式还是很舒服的。
写在最后:让阅读回到阅读本身
我一直觉得,好的自托管软件不应该让人每天都在和它搏斗。
它应该像房间角落里的一盏灯:需要时一按就亮,不需要时安静地待在那里。Lumos Reader 想做的就是这种感觉——书库依然在自己的 NAS 里,数据不必被拆散到各个平台,换一台设备也能从上次停下的地方继续读。
如果说 NAS 是书库城堡,Lumos Reader 就是那条从城堡通往日常生活的传送门。电脑前可以读,沙发上可以读,出门后也可以继续读;而真正重要的那本书,始终还是稳稳地留在自己的书架上。
项目开源地址:Alumos/LumosReader
把书留在 NAS,把阅读带在身边。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!














