Readest 跨设备同步修复实录:fileless 的 RSS 订阅书如何绕过 uploadedAt 门控(Issue 5307)
桌面应用跨平台前端【免费下载链接】readestReadest 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 仓库中的问题记忆文档 rss-feed-books-not-syncing-5307.md 展开结合services/rss、services/cloudService.ts、app/library/hooks/useBooksSync.ts等源码与配套测试完整还原 #5307 从现象、根因、修复到回归测试的排查链路。读完你将理解Readest 中无文件书籍fileless book的数据模型是什么、为什么 RSS 订阅永远无法同步到第二台设备、uploadedAt这个同步门控字段的真实语义以及isFeedBook谓词在哪些环节被接入才能让订阅真正跨设备可见。问题现象订阅只在添加它的浏览器里出现Issue #5307 报告于 web.readest.com 0.11.20复现条件非常简单同一账号登录两台笔记本浏览器在其中一台添加 RSS 订阅随后观察到两个症状手动触发上传到云upload to cloud立即失败另一台浏览器的书架上始终看不到这条订阅。表面上是两个独立故障实际排查后确认是同一个根因RSS 订阅在数据层被建模为一条没有文件的书籍记录而同步链路的多处逻辑默认书籍必须有文件于是订阅在源头无法上传、在目标端被静默丢弃。数据模型feed 订阅是一条无文件书籍记录Readest 的 RSS 订阅并不像普通电子书那样下载 EPUB 文件存入Books/hash/目录。查看 feedBook.ts 中createFeedBook的实现export function createFeedBook(feedUrl: string, parsed: ParsedFeed): Book { const now Date.now(); return { hash: feedBookHash(feedUrl), url: buildFeedBookUrl(feedUrl), format: EPUB, title: parsed.title, author: , metadata: { title: parsed.title, author: , language: , feedUrl }, createdAt: now, updatedAt: now, downloadedAt: now, uploadedAt: null, deletedAt: null, }; }关键特征一目了然url不是真实文件路径而是feed://协议描述符descriptormetadata.feedUrl携带真正的订阅地址uploadedAt: null、downloadedAt: now——即已下载生成但从未上传没有任何 blob 文件落盘全书库中找不到这条订阅对应的物理文件。feed://描述符的编解码在 feedBookUrl.tsexport const FEED_SCHEME feed://; export const isFeedBookUrl (url: string): boolean url.startsWith(FEED_SCHEME); export const buildFeedBookUrl (feedUrl: string): string FEED_SCHEME encodeURIComponent(JSON.stringify({ feedUrl })); export const parseFeedBookUrl (url: string): { feedUrl: string } JSON.parse(decodeURIComponent(url.replace(FEED_SCHEME, )));即feed://后跟 URL 编码的{feedUrl:https://...}JSON。feedBookHash则对该描述符做 md5 得到书籍 hash保证同一 feed 在任何设备上生成的 hash 一致feedBook.ts。正是因为没有物理文件任何一台设备打开该订阅时readerStore都是根据 feed URL 现场重建书籍内容而对端同步拉取时transformBookFromDButils/transform.ts也会用metadata.feedUrl重新水合rehydrate出book.url。文件可以不存在但描述符必须在。根因拆解上传抛错 拉取被门控 订阅永远留在原设备第一步上传直接抛错同步失败只是表象用户看到的上传到云失败源于 cloudService.ts 的uploadBooklet bookSource await resolveBookContentSource(fs, book); // ...url 分支先把外部文件拷进 Books/ 再转成 managed... if (!isBookFileContentSource(bookSource)) { throw new Error(Book file not uploaded); }resolveBookContentSourcebookContent.ts按优先级探测书籍内容来源优先Books/hash/下的受管副本其次book.filePath外部文件、content://URI最后看book.url。而book.url是feed://开头时返回的是if (isFeedBookUrl(book.url)) { return { kind: feed, path: book.url, base: None }; }BookContentSource联合类型里明确存在{ kind: feed }这一分支bookContent.ts但isBookFileContentSource只认managed | external | url三种export function isBookFileContentSource( source: BookContentSource, ): source is BookFileContentSource { return source.kind managed || source.kind external || source.kind url; }于是feed类型被判定为非文件内容源uploadBook抛出Book file not uploaded同步队列里的上传任务失败——这就是界面上可见的同步失败。而uploadedAt因为上传从未成功永远保持null。第二步元数据其实已经上云但对端不肯接单有意思的是订阅的元数据行其实已经成功同步到了云端推送侧useBooksSync.getNewBooks只看syncedAt/updatedAt与uploadedAt无关订阅行照常进入推送队列见 useBooksSync.ts服务端拉取接口也没有uploaded_at过滤。真正卡住订阅的是拉取侧的采纳门控。useBooksSync.updateLibrary处理从云端拉回的新书时过滤条件是useBooksSync.tsconst newBooks cloudBooks.filter( (newBook) !bookHashesInLibrary.has(newBook.hash) (newBook.uploadedAt || isFeedBook(newBook) || isAudiobook(newBook)) !newBook.deletedAt, );这个newBook.uploadedAt检查是一个有意的保护普通书如果没有上传过文件对端即使收到了元数据行也拿不到文件本体把这样的行放进书架只会产生一本打不开的书。然而 feed 订阅恰好不需要文件——内容可以从metadata.feedUrl重建。保护逻辑对普通书成立却把 RSS 订阅错杀了订阅被静默丢弃永远不会出现在除添加设备以外的任何设备上。一句话总结根因上传侧因 feed 无文件而必然失败 →uploadedAt永远为 null → 拉取侧的必须有文件才采纳门控把无文件的订阅误判为不可获取 → 订阅数据到了云端却落不了地。修复方案isFeedBook 谓词 全链路接入点修复的核心是一个新谓词isFeedBook(book)feedBookUrl.tsexport const isFeedBook (book: { url?: string }): boolean !!book.url isFeedBookUrl(book.url);它的语义很明确这是一条内容源自feed://描述符、无需物理文件的书籍。随后把它接入所有基于书籍文件做决策的地方——记忆文档称之为the places that reason about a books FILE。同步修复点核心updateLibrary的新书过滤改为(newBook.uploadedAt || isFeedBook(newBook))让无文件的 feed 订阅绕过uploadedAt门控useBooksSync.ts。同时processNewBook里对 feed 书走专门的封面处理分支newBook.coverImageUrl appService isFeedBook(newBook) ? await ensureFeedBookCover(appService, newBook) : await appService?.generateCoverImageUrl(newBook);封面本地再生ensureFeedBookCover普通新书的封面从云端下载downloadBookCovers但 feed 书的uploadBook从未运行云端根本没有它的封面文件。因此新增ensureFeedBookCover从handleAddFeedSubmit中原有的封面逻辑抽取对端根据feedUrl title在本地重新生成完全一致的封面feedBook.tsexport async function ensureFeedBookCover( appService: FeedCoverWriter, book: Book, ): Promisestring | undefined { try { const feedUrl book.metadata?.feedUrl ?? (book.url ? parseFeedBookUrl(book.url).feedUrl : ); const coverFilename getCoverFilename(book); if (!(await appService.exists(coverFilename, Books))) { const pngBytes await rasterizeCoverSvg(generateFeedCoverSvg(feedUrl, book.title)); await appService.createDir(book.hash, Books, true); await appService.writeFile(coverFilename, Books, pngBytes); } return await appService.generateCoverImageUrl(book); } catch (e) { console.warn(Failed to generate feed book cover:, e); return undefined; } }封面由generateFeedCoverSvg生成经典 RSS 橙色图标 站点名 订阅标题其中站点名取自new URL(feedUrl).hostname去www.前缀feedBook.ts。由于输入只有feedUrl和title任何设备生成的封面字节一致天然满足跨设备一致性随后rasterizeCoverSvg把 SVG 先画到 canvas 再导出 PNG——源码注释解释了原因书架封面存储在Books/hash/cover.png而 Android WebView 不会渲染以 .png 扩展名提供的 SVG 字节feedBook.ts。该函数是 best-effort 的失败时退回书架仅标题的兜底封面不影响订阅本身。文件操作入口全面收口凡是可能对 feed 书发起下载/上传/分享的界面与逻辑全部用isFeedBook屏蔽否则用户会看到注定失败的操作从源码可以确认以下接入点位置改动意图useBooksSync.ts新书过滤 封面本地再生同步修复本体libraryUtils.tsgetBookContextMenuItemIds不再对 feed 书提供下载/上传/分享菜单项BookItem.tsx书架云状态徽章不再展示待上传/需下载等误导状态TransferQueuePanel.tsxUpload All 批量上传排除 feed 书BookDetailView.tsx详情页上传按钮对 feed 书隐藏记忆文档中还提到getBookContextMenuItemIds的修改no download/upload/share在 libraryUtils.ts 附近有对应判断。被否决的替代方案为什么不用更简单的做法排查过程中提出过两个更省事的候选方案均被否决理解它们的缺陷有助于避免未来踩坑创建时直接盖章uploadedAt行不通。uploadedAt的语义是文件已在云端就绪的时间点对端看到它就会认为可以下载文件进而给用户显示下载入口、去抓取一个根本不存在的文件——制造出新的坏体验。把downloadedAt置为 null 代替过于隐晦downloadedAt与文件有无并无直接关系而且无法修复已经在用户书库中存在的存量 feed 书——那些行早已生成改动创建逻辑对它们无效。相比之下显式的isFeedBook谓词语义清晰、可覆盖存量数据是正确解。回归测试五种行为被固化修复伴随的测试在 feed-books-sync.test.tsx用renderHook 模拟的syncBooks/ensureFeedBookCover覆盖了完整行为矩阵无uploadedAt的 feed 书也能被采纳上架核心回归订阅跨设备可见feed 书的封面走ensureFeedBookCover本地再生而非云端下载且封面 URL 正确写入书架普通书仍走generateCoverImageUrl云端封面路径不受影响普通书没有uploadedAt时依旧被跳过——原保护逻辑对普通书保持原样修复没有放宽文件书的安全性pushLibrary会把 feed 书推送到云端——元数据行确实上云只是文件不上传。配合 feedBookUrl.test.ts 对feed://描述符的 round-trip 与负例pse://、https://不被误判为 feed测试谓词本身的正确性也有保障。遗留问题与后续工作记忆文档明确标注了本次修复没有覆盖的边界与已知问题文件同步后端优雅降级在 WebDAV / Drive / S3 等文件同步后端下feed 书的pushBookFile会解析出no-source并跳过不会报错——这是符合预期的降级行为无需处理分享对话框本就安全ShareBookDialog的shareEnabled以fileSize ! null门控feed 书天然不满足不会出现无效分享入口疑似死代码FeedsView与feedStore对应Settings/feeds.json经排查可能是死代码——handleShowFeeds打开的是AddFeedModal没有任何地方设置showFeeds因此订阅的逐条已读状态实际上根本没有跨设备同步。这是一个已知但未在本 issue 中修复的缺口关联 issue该问题与opds-catalog-reincarnate-restart-5180OPDS 目录相关、demo-books-cloud-sync-5049demo 书不上云见 useBooksSync.ts 中isDemoBook过滤同属同步链路对特殊书型考虑不足这一类问题。结语#5307 是一次典型的数据模型与同步门控语义冲突问题feed://无文件书籍挑战了书籍文件的默认假设uploadedAt门控虽然保护了普通书不被孤儿上架却误伤了本不需要文件的 RSS 订阅。修复没有削弱原有保护普通书仍需uploadedAt才能被采纳而是为无文件书型打开显式豁免通道并把所有围绕文件做决策的 UI 入口一并收口最终以 5 条回归测试固化行为。对于阅读本文的读者这条排查链路的价值在于当同步系统中出现元数据已到、文件未到的异常状态时先检查采纳侧的门控字段与特殊书型的数据模型是否冲突再决定是放宽门控还是新增显式谓词。相关源码与文档索引问题记忆 rss-feed-books-not-syncing-5307.md、描述符与谓词 feedBookUrl.ts、订阅书构造与封面 feedBook.ts、同步主逻辑 useBooksSync.ts、内容源解析 bookContent.ts、上传入口 cloudService.ts、回归测试 feed-books-sync.test.tsx。赞分享桌面应用跨平台前端【免费下载链接】readestReadest 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 同步类别门控与捆绑式设置副本字典偏好为何会绕过 Dictionaries 开关5465 修复实录Readest 同步类别门控与捆绑式设置副本字典偏好为何会绕过 Dictionaries 开关 5465 修复实录 导读 本文围绕 Readest桌面应用跨平台前端跨平台阅读革命Readest如何实现全设备无缝同步跨平台阅读革命Readest如何实现全设备无缝同步 在数字阅读时代读者常常面临一个痛点在手机上读到一半的电子书切换到平板或电脑时不仅需要重新寻找文件桌面应用跨平台前端Readest 跨设备同步故障修复实录从云同步 Provider 选择到 CRDT 冲突合并的工程实践Readest 跨设备同步故障修复实录从云同步 Provider 选择到 CRDT 冲突合并的工程实践 导读 本文基于 Readest 开源仓库中沉淀的同步问桌面应用跨平台前端上一篇Claude HUD重新定义Claude Code开发体验的实时状态监控系统下一篇Tolaria 原始编辑器语法高亮实践以 codemirror/lang-markdown 替代正则装饰ADR-0037创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

Claude Code Commands模块:AI辅助Go开发实战解析

Claude Code Commands模块:AI辅助Go开发实战解析

1. Claude Code Commands 模块深度解析作为一名长期从事AI辅助开发工具研究的工程师,我发现Claude Code的Commands模块设计极具实用价值。这个模块不是简单的命令集合,而是一个经过实战检验的AI辅助开发工作流系统。1.1 模块架构设计理念Commands模块的核…

2026/9/21 15:27:29 阅读更多 →
理解Bull锁机制与stalled任务恢复:at-least-once语义下的分布式任务安全之道

理解Bull锁机制与stalled任务恢复:at-least-once语义下的分布式任务安全之道

理解Bull锁机制与stalled任务恢复:at-least-once语义下的分布式任务安全之道 【免费下载链接】bull Premium Queue package for handling distributed jobs and messages in NodeJS. 项目地址: https://gitcode.com/gh_mirrors/bu/bull Bull 是一款基于 Redi…

2026/9/21 15:27:29 阅读更多 →
Flask数据库操作全指南:从基础到生产环境优化

Flask数据库操作全指南:从基础到生产环境优化

1. Flask数据库操作基础概念Flask作为轻量级Python Web框架,数据库操作是其核心功能之一。与Django自带ORM不同,Flask需要借助扩展来实现数据库交互,这种设计既保持了核心简洁,又提供了灵活的扩展选择。我刚开始用Flask时&#xf…

2026/9/21 15:27:29 阅读更多 →

最新新闻

Swagger Codegen Bash 客户端模型文档解读:以 Petstore 的 Category 模型为例

Swagger Codegen Bash 客户端模型文档解读:以 Petstore 的 Category 模型为例

开发工具代码生成API设计 【免费下载链接】swagger-codegen swagger-codegen contains a template-driven engine to generate documentation, API clients and server stubs in different languages by parsing your OpenAPI / Swagger definition. 项目地址: http…

2026/9/21 15:50:58 阅读更多 →
GraalVM Native Image 运行时模块系统支持(Runtime Module System)深度解析

GraalVM Native Image 运行时模块系统支持(Runtime Module System)深度解析

编译器JIT编译语言运行时高性能计算内存管理 【免费下载链接】graal GraalVM compiles applications into native executables that start instantly, scale fast, and use fewer compute resources 🚀 项目地址: https://gitcode.com/gh_mirrors/gr/gra…

2026/9/21 15:50:58 阅读更多 →
Nix 二进制缓存签名密钥对生成指南:nix-store --generate-binary-cache-key 详解

Nix 二进制缓存签名密钥对生成指南:nix-store --generate-binary-cache-key 详解

Nix 二进制缓存签名密钥对生成指南:nix-store --generate-binary-cache-key 详解 【免费下载链接】nix Nix, the purely functional package manager 项目地址: https://gitcode.com/gh_mirrors/ni/nix Nix 的二进制缓存(binary cache&#xff09…

2026/9/21 15:50:58 阅读更多 →
MCP Python SDK 客户端回调(Client Callbacks)权威指南:响应服务端发起的请求与能力协商

MCP Python SDK 客户端回调(Client Callbacks)权威指南:响应服务端发起的请求与能力协商

MCP Python SDK 客户端回调(Client Callbacks)权威指南:响应服务端发起的请求与能力协商 【免费下载链接】python-sdk The official Python SDK for Model Context Protocol servers and clients 项目地址: https://gitcode.com/gh_mirrors…

2026/9/21 15:50:58 阅读更多 →
DLSS Swapper 完整指南:免费两步替换游戏内 DLSS 版本

DLSS Swapper 完整指南:免费两步替换游戏内 DLSS 版本

DLSS Swapper 完整指南:免费两步替换游戏内 DLSS 版本 【免费下载链接】dlss-swapper 项目地址: https://gitcode.com/GitHub_Trending/dl/dlss-swapper 一个很常见的情况:游戏大更新后,你重新进入游戏,发现画质比之前更糊…

2026/9/21 15:50:58 阅读更多 →
MyBatis缓存优化与EHCache集成实战

MyBatis缓存优化与EHCache集成实战

1. MyBatis缓存机制与EHCache的价值解析作为Java生态中最受欢迎的ORM框架之一,MyBatis的缓存设计直接影响着应用性能。其内置的PerpetualCache采用简单的HashMap实现,在单机环境下表现尚可,但在分布式场景或高并发请求下就会暴露出内存限制、…

2026/9/21 15:49:57 阅读更多 →

日新闻

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程 【免费下载链接】agentic-awesome-skills AAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and …

2026/9/21 0:00:01 阅读更多 →
gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析

gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析

gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析 【免费下载链接】gin-vue-admin 🚀ViteVue3Gin拥有AI辅助的基础开发平台,企业级业务AI开发解决方案,内置mcp辅助服务,内置skills管理,…

2026/9/21 0:00:01 阅读更多 →
Wox 全功能插件开发实战指南:基于 Python / Node.js 宿主与 WebSocket 的持久化插件体系

Wox 全功能插件开发实战指南:基于 Python / Node.js 宿主与 WebSocket 的持久化插件体系

桌面应用AI 应用插件系统 【免费下载链接】Wox A cross-platform launcher that simply works 项目地址: https://gitcode.com/gh_mirrors/wo/Wox 点击查看 免费下载 全功能插件(Full-featured Plugin)是 Wox 三类插件实现方式中能力最完整的…

2026/9/21 0:00:01 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/21 3:13:20 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/21 2:19:36 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/21 4:51:05 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/21 15:36:51 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/21 15:36:51 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/19 23:35:34 阅读更多 →