Readest 原生 Grimmory(Booklore)同步集成方案:API 表面、CORS 分析与书目身份映射策略
2026/9/20 12:54:29 网站建设 项目流程
  • 桌面应用
  • 跨平台
  • 前端

【免费下载链接】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.

项目地址:https://gitcode.com/gh_mirrors/re/readest
点击查看免费下载

本篇文章基于 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/loginPOST{username, password}{accessToken, refreshToken, expires}
POST /api/v1/auth/refreshPOST{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-KeyX-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

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/refreshSecurityConfig.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.tssrc/services/sync/KOSyncClient.tsconnect/getProgress/updateProgress三个核心方法,对应 KOSyncClient 的connect(登录 + 注册)、getProgressGET /syncs/progress/{hash})、updateProgressPUT /syncs/progress
src/app/reader/hooks/useGrimmorySync.tssrc/app/reader/hooks/useKOSync.ts拉取 / 推送 / 冲突处理的状态机,监听push-kosyncpull-kosyncflush-kosync事件,打开书籍时拉取一次、进度变化时防抖(5000ms)自动推送、窗口失焦时 flush
GrimmorySettingsKOSyncSettings(src/types/settings.ts)服务器地址、用户名、token、设备名等配置结构
GrimmoryForm.tsxKOSyncForm(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;反过来applyRemoteProgressgetCFIFromXPointer把远端 XPointer 还原为 CFI 再view.goTo(cfi)。原生 Grimmory 路径因为两端都是 CFI,转换环节更少,但仍然需要处理畸形 CFIisMalformedLocationCfi分支回退到上次已知良好的位置)和固定版式(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 分离

  1. 本地 SQLite 表book(book_path, partial_md5, grimmory_id),每个文件同时存两个 ID。
  2. 路径一(grimmory 原生 ID)grimmory_id=book.id,用于会话(sessions)/下载/书架。解析方式是只按 ISBN13/ISBN10/ISBN/ASIN 精确匹配doc_metadata.luaisBook判定),不按书名/作者匹配;匹配成功后通过repository.upsertBook(path, book.id)持久化。会话上报走POST /api/v1/reading-sessions,以grimmory_id为键。
  3. 路径二(阅读进度)仍然走 KOReader 兼容端点GET/PUT /api/koreader/syncs/progress[/{partialMD5}],以 KOReader 自己的util.partialMD5(book_path)为键(不是 grimmory_id)。凭证由原生接口GET/PUT /api/v1/koreader-users/me自动配发(getKoreaderCredentialsmd5(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 的 LuaJITbit.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 采集而来(例如本地导入)时,需要按如下优先级做身份匹配(文档给出的原生路径排序):

  1. OPDS 采集期捕获(权威):从下载链接直接拿到bookId+fileId
  2. 缓存 ID:此前匹配成功过的(path, grimmory_id)映射;
  3. ISBN / ASIN 精确匹配:唯一被官方插件验证的元数据字段;
  4. 受限的 书名+作者 匹配:必须额外要求格式(format)与fileSizeKb一致才放行,遇到歧义时必须弃权—— 因为错误匹配会污染另一本书的进度fileSizeKb在 BookFile 上有暴露,可作为尺寸佐证;哈希不暴露

文档特别强调了策略 4 的纪律:宁可不同步,不可错同步。这与 useKOSync.ts 中"远端 XPointer 无法本地解析时保持 unresolved 状态、绝不拿百分比硬凑"的冲突处理哲学一脉相承。

十、结论与工程启示

这份设计分析文档的价值在于它是一份可执行的失败复盘:完整记录了 API 契约、逐行级 CORS 分析、端侧分化结论、扩展点模板映射、以及两轮身份匹配研究(官方 koplugin 实证 + OPDS 采集期捕获设计)。对后续任何要重新发起 Grimmory 原生同步的开发者,它直接给出了四条可落地结论:

  1. 认证走 JWT 原生接口,但 Web 端登录必须过代理SecurityConfig.java的白名单排除导致/api/v1/auth/login无 CORS),模板是 src/pages/api/kosync.ts;
  2. 进度同步优先复用 KOSync 的 partial-MD5 / XPointer 机制打/api/koreader/**,这是官方 koplugin 验证过的路径;但在依赖前必须核验 Java1024L << -2与 LuaJITbit.lshift(1024,-2)的哈希采样差异;
  3. 身份匹配以 OPDS 采集期捕获为最优解urn:booklore:*+ 双 ID 下载链接),落点已具备upsertOPDSSourceMapping基础设施;元数据兜底匹配必须用 ISBN/ASIN 或"书名+作者+格式+文件大小"四元组并严格弃权;
  4. 扩展点全部以现有 KOSync 实现为模板GrimmoryClientKOSyncClientuseGrimmorySyncuseKOSyncGrimmoryFormKOSyncFormGrimmorySettingsKOSyncSettings),进度字段 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.

项目地址:https://gitcode.com/gh_mirrors/re/readest
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询