- 桌面应用
- 跨平台
- 前端
【免费下载链接】readest
Readest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.
本篇文章基于 Readest 仓库中的设计分析文档《Grimmory Native Sync》(apps/readest-app/.claude/memory/grimmory-native-sync.md),完整梳理在 Readest 中为Grimmory(Booklore 的一个分支,Java/Spring 后端,包名org.booklore)构建原生阅读进度同步的完整技术方案。你将掌握 Grimmory 原生 REST API 的认证与进度协议、其 Spring Security CORS 配置的隐藏陷阱(登录端点在浏览器跨域场景下不可用的根因)、官方 KOReader 插件(koplugin)的身份映射机制,以及 Readest 侧"以 KOSync 为模板"的扩展点设计与 OPDS 采集期身份捕获这一最佳匹配策略。
一、背景:为什么要"原生同步",而不是继续走 OPDS + KOReader 兼容路径
Readest 此前接入 Grimmory 的方式是借助OPDS 目录 + KOReader 兼容协议绕行。该方案存在一个结构性问题:同一本书的阅读进度会在KOReader ↔ Kobo ↔ Grimmory三个生态之间来回流转,形成三方数据不同步(desync)。文档明确指出,讨论见 grimmory-tools/discussions/1417。
绕行路径的问题在于:KOReader 兼容端点(/api/koreader/**)以 KOReader 自己的身份体系(X-Auth-User+X-Auth-Key)和进度格式(CREngine XPointer)工作,而 Grimmory 原生体系使用 JWT 认证与自有的BookFileProgress结构。两种体系间的转换层越多,进度错位、身份误判的概率就越高。
因此目标很清晰:接入 Grimmory 的原生 API(/api/v1/**),替换掉 KOReader 兼容绕行路径。
二、Grimmory 原生 API 表面(Native API Surface)
方案的核心是直接消费 Grimmory 的原生 REST API,文档记录了以下接口全貌:
2.1 认证(JWT Bearer)
| 端点 | 方法 | 请求体 | 响应 |
|---|---|---|---|
POST /api/v1/auth/login | POST | {username, password} | {accessToken, refreshToken, expires} |
POST /api/v1/auth/refresh | POST | {refreshToken} | 刷新后的 token 三元组 |
Token 有效期设计为accessToken 2 小时、refreshToken 30 天,符合典型的短期访问令牌 + 长期刷新令牌模式。所有后续数据端点均携带Authorization: Bearer <accessToken>。
2.2 阅读进度与书目
- 进度上报:
POST /api/v1/books/progress,请求体为:{ "bookId": "<book.id>", "fileProgress": { "bookFileId": "<bookFile.id>", "progressPercent": 0-100, "positionData": "<CFI for EPUB>", "positionHref": "<spine href>", "ttsPositionCfi": "<TTS 朗读位置>" }, "dateFinished": "<完成日期>" }其中
positionData对 EPUB 使用CFI(Canonical Fragment Identifier)定位;ttsPositionCfi用于同步 TTS 朗读位置;progressPercent取值范围 0-100。 - 书目列表:
GET /api/v1/books。 - 关键限制:书目 DTO不暴露文件哈希,也没有原生的按哈希查询端点。这意味着无法像 KOSync 那样直接以文件哈希为键做进度关联,必须退而求其次按元数据匹配(详见第五节)。
- 注释/书签/下载/封面:
- 注释:
/api/v1/annotations/** - 书签:
/api/v1/bookmarks/** - 下载:
/api/v1/books/{id}/download(支持 HTTP Range 断点续传) - 封面:
/api/v1/media/{id}/cover
- 注释:
2.3 KOReader 兼容路径(将被替换的旧方案)
Grimmory 仍保留/api/koreader/**兼容端点,使用X-Auth-User+X-Auth-Key(X-Auth-Key = md5(password))头认证。这是当前 Readest 绕行路径所依赖的接口,也是本次原生同步改造要替换掉的部分。
三、CORS 分析:Spring Security per-filter-chain 配置的隐藏陷阱
文档对 Grimmory 后端的SecurityConfig.java做了逐行级分析,这是整个方案中最容易出现"浏览器端连不上、桌面端却一切正常"诡异现象的部分。
3.1 配置事实
- 没有全局 CorsFilter / addCorsMappings,CORS 策略逐 filter-chain 独立配置。
- 策略位于
SecurityConfig.java约 340-368 行:- origins 默认
*(可用环境变量ALLOWED_ORIGINS覆盖),且使用setAllowedOriginPatterns,因此*可以与 credentials 并存; - 允许所有 HTTP 方法;
- allowed-headers 是固定白名单:
Authorization, Cache-Control, Content-Type, Range, If-None-Match, If-Modified-Since—— 不是*,并且明确排除了X-Auth-User/X-Auth-Key; allowCredentials = true。
- origins 默认
3.2 哪些链路开了 CORS,哪些没开
- 开了
.cors()的链:jwtApi(order 10,匹配/api/**减去白名单外的路径,覆盖 books/progress/annotations/bookmarks/reading-sessions/koreader-users);bookDownload(order 8);- epub / audiobook / custom-font / ws(order 5-9)。
- 没开
.cors()的链:opds(order 1)、komga(order 2)、koreader(order 3)、kobo(order 3)、media/cover(order 4)、catch-all static(order 11)。
3.3 关键缺口:登录端点无 CORS
/api/v1/auth/login与/auth/refresh在SecurityConfig.java约 265-289 行被从 order 10 的 matcher 中白名单排除(它们不要求 JWT),因此落入了 order 11 的 catch-all 静态资源链 ——该链没有 CORS 配置。结论是:
跨域浏览器登录必然失败(预检请求拿不到允许的跨域响应头)。
而这一点对 Grimmory 自带的 SPA 不可见,因为它的前端与后端同源部署(classpath:/static/提供静态资源),浏览器根本不会触发跨域。只有像 Readest Web 这样的第三方跨域客户端才会踩中这个坑。
四、对 Readest 各端的影响:Tauri 原生与 Web 构建的分化
文档给出了明确的端侧结论,这是方案落地的关键分叉:
- Tauri 桌面 / 移动端:CORS 完全无关。Readest 在 Tauri 下使用
@tauri-apps/plugin-http的原生 HTTP 栈(见 KOSyncClient.ts 中对tauriFetch的使用),不受浏览器同源策略约束,所有端点(包括登录)均可直连。 - Readest Web 构建:JWT 数据端点拿到 token 后可以跨域工作,但登录(login)和 koreader 端点必须经过服务端代理或同源反向代理 —— 这与 Readest 现有的
/api/kosync、/api/opds/proxy是同一个模式。
Readest 现有的 KOSync 代理实现 src/pages/api/kosync.ts 正是可复用的模板:它接收{serverUrl, endpoint, method, headers, body},对 endpoint 做正则白名单校验(/\/users\/create/, /\/users\/auth/, /\/syncs\/progress/)、强制http/https协议、通过isLanAddress拦截内网地址(SSRF 防护),再把请求转发到目标服务器并透传状态码。一个/api/grimmory代理只需沿用同样的校验骨架,把白名单换成/api/v1/auth/login、/api/v1/auth/refresh等即可。
五、Readest 侧扩展点设计(以 KOSync 为模板)
文档为原生同步预留了一组与现有 KOSync 功能一一对应的扩展点,全部以仓库中真实存在的 KOSync 实现为模板:
| 新组件(计划) | 模板(仓库现有实现) | 职责 |
|---|---|---|
src/services/grimmory/GrimmoryClient.ts | src/services/sync/KOSyncClient.ts | connect/getProgress/updateProgress三个核心方法,对应 KOSyncClient 的connect(登录 + 注册)、getProgress(GET /syncs/progress/{hash})、updateProgress(PUT /syncs/progress) |
src/app/reader/hooks/useGrimmorySync.ts | src/app/reader/hooks/useKOSync.ts | 拉取 / 推送 / 冲突处理的状态机,监听push-kosync、pull-kosync、flush-kosync事件,打开书籍时拉取一次、进度变化时防抖(5000ms)自动推送、窗口失焦时 flush |
GrimmorySettings | KOSyncSettings(src/types/settings.ts) | 服务器地址、用户名、token、设备名等配置结构 |
GrimmoryForm.tsx | KOSyncForm(IntegrationsPanel.tsx 中subPage === 'kosync'分支) | 设置页集成表单,在 IntegrationsPanel 的 subPage 路由中注册 |
5.1 进度映射是核心难点
Readest 的BookProgress.location存的是CFI,Grimmory 的BookFileProgress.positionData也是CFI,这是二者天然对齐的地方;而 Grimmory 后端另有EpubCfiService负责CFI ↔ XPointer双向转换。
对照现有实现,useKOSync.ts 中的generateKOProgress展示了 Readest 侧如何把内部进度转成协议格式:从progress.location取 CFI,经getXPointerFromCFI转为 XPointer 并缓存到config.xpointer;反过来applyRemoteProgress用getCFIFromXPointer把远端 XPointer 还原为 CFI 再view.goTo(cfi)。原生 Grimmory 路径因为两端都是 CFI,转换环节更少,但仍然需要处理畸形 CFI(isMalformedLocationCfi分支回退到上次已知良好的位置)和固定版式(FXL)书籍按页码而不是 CFI 定位这两类边界情况,这些逻辑在 KOSync hook 中已有成熟实现,可直接复用。
六、实现状态:垂直切片已构建后按维护者要求回滚
文档如实记录了这段工作的状态:未发布(NOT shipped):
2026-06-23 曾构建完整垂直切片 ——
GrimmoryClient+useGrimmorySynchook +/api/grimmory代理 +GrimmoryForm/IntegrationsPanel 集成 + settings/types 类型 + BookConfig 中缓存元数据匹配身份 + 原生/api/v1/books/progress上报,测试与 lint 全绿 —— 但随后按维护者要求回滚("not ready yet")。工作树已完全恢复(grimmory 相关文件全部删除,7 个被改动的共享文件已还原,lint + test 恢复通过)。
回滚的原因是:原生进度的身份匹配方案(identity story)被判定还不够成熟—— 更稳健的路径(OPDS 采集期捕获、或镜像官方 koplugin 的做法)当时尚未构建。因此文档将本次分析沉淀为两条核心发现(FINDING A / FINDING B)以备后续重试。
七、FINDING A:官方 koplugin 的身份映射机制(它并不使用原生进度接口)
官方插件(grimmory-tools/grimmory.koplugin)的实践是文档最重要的外部参照系,其核心设计是双 ID 分离:
- 本地 SQLite 表:
book(book_path, partial_md5, grimmory_id),每个文件同时存两个 ID。 - 路径一(grimmory 原生 ID):
grimmory_id=book.id,用于会话(sessions)/下载/书架。解析方式是只按 ISBN13/ISBN10/ISBN/ASIN 精确匹配(doc_metadata.lua的isBook判定),不按书名/作者匹配;匹配成功后通过repository.upsertBook(path, book.id)持久化。会话上报走POST /api/v1/reading-sessions,以grimmory_id为键。 - 路径二(阅读进度):仍然走 KOReader 兼容端点
GET/PUT /api/koreader/syncs/progress[/{partialMD5}],以 KOReader 自己的util.partialMD5(book_path)为键(不是 grimmory_id)。凭证由原生接口GET/PUT /api/v1/koreader-users/me自动配发(getKoreaderCredentials→md5(secret)→X-Auth-User/X-Auth-Key)。
结论:官方插件中被验证过的进度路径,恰恰是复用现有 KOSync 的 XPointer / partial-MD5 机制去打/api/koreader/...,而不是原生进度 API。对 Readest 而言,这意味着已有的 KOSync 基础设置可以原样迁移。
7.1 必须验证的哈希陷阱:Java 移位溢出 vs LuaJIT 移位
文档记录了一个非常隐蔽的兼容性风险点,值得任何实现者先行验证:
- 后端
FileFingerprint.generateHash在采样时计算1024L << -2:Java 中 long 位移量超出范围会被掩码,-2最终落到偏移0; - 而 KOReader 的 LuaJIT
bit.lshift(1024, -2)得到偏移256;
两者对文件的第一块采样位置可能不一致,导致同一文件算出的 partial-MD5 不同 ⇒按哈希的进度同步可能静默失配。文档明确要求:在依赖该路径前,拿一个真实下载的文件用两种方式各算一次哈希进行核验。
八、FINDING B:OPDS 采集期捕获 —— 最优身份策略(尚未构建)
这是文档选定的"最佳匹配"方案,核心洞察是Grimmory 的 OPDS 源在采集下载时就把两个 ID 都暴露出来了:
- OPDS 指纹(
OpdsFeedService.java):每个<id>都是urn:booklore:*(根urn:booklore:root、书目urn:booklore:book:{bookId});feed 标题为Booklore Catalog;self/start 链接指向/api/v1/opds。 - 采集链接同时编码两个 ID:
<link href="/api/v1/opds/{bookId}/download?fileId={fileId}" rel="http://opds-spec.org/acquisition"/>路径中的
bookId+ 查询参数fileId一次全部拿到。
Readest 侧的实现落点已经有现实基础:OPDS 页面 src/app/opds/page.tsx 在下载书目时持有url,且仓库已实现upsertOPDSSourceMapping(src/services/opds/sourceMap.ts),把(catalogId, sourceUrl, bookHash)写入opds_source_mappings表(ON CONFLICT(catalog_id, source_url) DO UPDATE),并可经findBookByOPDSSources反查书目。原生同步只需在 OPDS 下载点解析bookId(path)与fileId(query),再用urn:booklore:条目 ID 或与配置的 grimmory serverUrl 同源来佐证,把两个 ID 写入 BookConfig 即可。
为什么这是最优解:身份来自权威来源(Grimmory 自己下发的链接),无需任何元数据猜测,对 Grimmory 来源的书目而言是唯一不会错配的身份策略。
九、身份匹配策略完整排序
当书籍不是经 Grimmory OPDS 采集而来(例如本地导入)时,需要按如下优先级做身份匹配(文档给出的原生路径排序):
- OPDS 采集期捕获(权威):从下载链接直接拿到
bookId+fileId; - 缓存 ID:此前匹配成功过的
(path, grimmory_id)映射; - ISBN / ASIN 精确匹配:唯一被官方插件验证的元数据字段;
- 受限的 书名+作者 匹配:必须额外要求格式(format)与
fileSizeKb一致才放行,遇到歧义时必须弃权—— 因为错误匹配会污染另一本书的进度。fileSizeKb在 BookFile 上有暴露,可作为尺寸佐证;哈希不暴露。
文档特别强调了策略 4 的纪律:宁可不同步,不可错同步。这与 useKOSync.ts 中"远端 XPointer 无法本地解析时保持 unresolved 状态、绝不拿百分比硬凑"的冲突处理哲学一脉相承。
十、结论与工程启示
这份设计分析文档的价值在于它是一份可执行的失败复盘:完整记录了 API 契约、逐行级 CORS 分析、端侧分化结论、扩展点模板映射、以及两轮身份匹配研究(官方 koplugin 实证 + OPDS 采集期捕获设计)。对后续任何要重新发起 Grimmory 原生同步的开发者,它直接给出了四条可落地结论:
- 认证走 JWT 原生接口,但 Web 端登录必须过代理(
SecurityConfig.java的白名单排除导致/api/v1/auth/login无 CORS),模板是 src/pages/api/kosync.ts; - 进度同步优先复用 KOSync 的 partial-MD5 / XPointer 机制打
/api/koreader/**,这是官方 koplugin 验证过的路径;但在依赖前必须核验 Java1024L << -2与 LuaJITbit.lshift(1024,-2)的哈希采样差异; - 身份匹配以 OPDS 采集期捕获为最优解(
urn:booklore:*+ 双 ID 下载链接),落点已具备upsertOPDSSourceMapping基础设施;元数据兜底匹配必须用 ISBN/ASIN 或"书名+作者+格式+文件大小"四元组并严格弃权; - 扩展点全部以现有 KOSync 实现为模板(
GrimmoryClient↔KOSyncClient、useGrimmorySync↔useKOSync、GrimmoryForm↔KOSyncForm、GrimmorySettings↔KOSyncSettings),进度字段 CFI 两端天然对齐,工程量可控。
Readest 侧可继续深入阅读的仓库资源:KOSyncClient.ts、useKOSync.ts、IntegrationsPanel.tsx、src/pages/api/kosync.ts、sourceMap.ts、xcfi 转换工具。
- 桌面应用
- 跨平台
- 前端
【免费下载链接】readest
Readest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.
相关推荐
Predis集群分片策略:Hash算法与SlotRange映射原理
Predis集群分片策略:Hash算法与SlotRange映射原理 你是否在使用Redis集群时遇到过数据分布不均、热点Key集中导致性能瓶颈的问题?Predi
数据库后端FillableLoaders 动画原理深入剖析:如何通过 CAShapeLayer 实现流畅进度动画
FillableLoaders 动画原理深入剖析:如何通过 CAShapeLayer 实现流畅进度动画 FillableLoaders 是一个基于 Swift
TypeSpec Java 客户端发射器:per-service api-version 映射的处理策略与单版本客户端生成原理
TypeSpec Java 客户端发射器:per service api version 映射的处理策略与单版本客户端生成原理 本文以 TypeSpec 仓库中
编程语言编译器后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考