Talebook 漫画阅读器接入契约:从页面 manifest 到安全图片分发的完整实现解析 后端前端CMS【免费下载链接】talebook一个简单好用的个人书库项目地址https://gitcode.com/gh_mirrors/ta/talebook点击查看免费下载Talebook个人书库在保留传统 EPUB 阅读器的同时引入独立的komga-reader作为漫画阅读前端并由后端通过一套“契约式”HTTP API 驱动。本文以仓库文档 document/ComicReaderApi.zh_CN.md 为核心结合后端实现 webserver/handlers/comic.py、资源受限的容器解析服务 webserver/services/comic_archive.py、前端 host 模板 webserver/resources/book/comic-reader.html 及测试 tests/test_comic_reader.py完整讲解阅读器接入契约、权限模型、页面级访问凭证、进度同步与安全资源预算。读完本文你将掌握如何在 Talebook 中接入独立漫画阅读器、如何正确调用四类 Comic API以及这套契约在防路径泄露与资源滥用上的具体工程手段。一、总体架构后端渲染 hostReader 只接触“公共视图”Talebook 的漫画阅读方案遵循“薄 host 独立 Reader”的分层设计。后端直接处理/read-comic/:bookId路由渲染webserver/resources/book/comic-reader.html并通过契约驱动独立的komga-reader。Reader 只接触页面 manifest、同源图片 URL 和阅读进度三个维度不会读取Calibre 路径、Talebook 数据库、可下载归档或归档条目名。这意味着所有“私有信息”都只存在于后端前端即使被完全审计也拿不到本地文件系统结构。1.1 前端分发静态产物不参与 npm/Docker 依赖Reader 沿用 Candle Reader 的静态 JavaScript 分发模式不作为 Talebook 的 npm 依赖参与 Nuxt/Docker 安装上游komga-reader构建出自包含的 ESM、UMD 与 CSS浏览器 bundle 内含隔离的 Vue runtime不依赖全局Vue、裸模块解析或 CDNTalebook 将不可变上游 commit 的产物、许可证和版本记录固定在app/public/static/komga-reader/实际包含komga-reader.es.js、komga-reader.umd.js、style.css、LICENSE、NOTICE、THIRD_PARTY_NOTICES当前固定版本记录在仓库根目录 komga-reader-version.txt值为d49a2e808601c7fc9b892a6c019a92eed017fd16。后端版本常量KOMGA_READER_VERSION与静态模块 URL 的?v查询参数共用同一值实现精确的缓存失效见 webserver/handlers/comic.py。1.2 路由边界/read-comic必须直达 Tornado/read-comic/:bookId是 Tornado 路由见 webserver/handlers/comic.py 中的routes()def routes(): return [ (r/read-comic/([0-9]), ComicReaderHandler), (r/api/book/([0-9])/comic/pages, ComicManifestHandler), (r/api/book/([0-9])/comic/pages/([0-9]), ComicPageHandler), (r/api/book/([0-9])/comic/progress, ComicProgressHandler), ]后端先完成登录、权限、书籍媒体类型与容器选择校验再渲染轻量 HTML host。Nginx 的 dev、SPA、SSR 配置及 Nuxt 本地开发代理都把这些路径直接转发给 Tornado。例如 conf/nginx/talebook.conf、conf/nginx/dev.conf、conf/nginx/server-side-render.conf 均使用location ~ ^/(api|get|read|read-comic|opds|auth|books|media)/ { proxy_pass http://backend; }关键约束该路径不能通过前端$backend添加/api前缀否则会破坏代理直通关系。后端模板从同源版本 URL 动态加载 ESM并在刷新或离页时调用destroy()Nuxt 中不存在app/pages/read-comic页面也不承担 Reader 生命周期或 API 适配。测试 tests/test_comic_reader.py 验证了 host 页面包含idcomic-reader-host、/static/komga-reader/komga-reader.es.js与 API 路径同时确保本地归档路径与page1.png等条目名不出现在响应体中。二、权限与支持范围manifest 与进度接口要求登录页面图片通常也接受登录 Cookie/Basic Auth。对应账号必须同时满足四条前置条件见 webserver/handlers/comic.py 的get_authorized_comic有在线阅读权限并已激活user.can_read()与user.is_active()能查看目标书籍——私有书只允许所有者和管理员_can_user_view_book校验item.scope private时必须是user.is_admin()或item.collector_id user.id目标书的media_type为comic至少含一个 CBZ、图片 ZIP、CBR 或图片 RAR 容器。容器选择遵循优先级CBZ ZIP CBR RAR由 webserver/services/comic_archive.py 的select_comic_container依据fmt_cbz、fmt_zip、fmt_cbr、fmt_rar字段顺序命中。漫画型 EPUB 不使用本契约继续进入现有 EPUB 阅读器。三、混合格式手动分类POST /api/book/:bookId/media_type当同一本书同时包含电子书格式EPUB、MOBI、AZW、AZW3、PDF、TXT和漫画容器CBZ、ZIP、CBR、RAR时所有者或管理员可在详情页“文件处理”菜单中选择“设置为漫画”或“设置为电子书”POST /api/book/:bookId/media_type Content-Type: application/json {media_type:comic}实现位于 webserver/handlers/book.py 的BookSetMediaTypemedia_type只接受comic或ebook其他值返回params.media_type目标书必须确实同时包含两类格式否则返回media_type.not_mixed“只有同时包含电子书和漫画格式的书籍才需要手动设置媒体类型”成功后服务端设置media_type_lockedtrue写入Item表后续目录扫描或上传新格式仍会执行文件安全分析但不会覆盖这个人工选择见 webserver/handlers/book.py 的_save_media_type仅当not item.media_type_locked时才自动合并再次选择另一类型即可修改单一类型书籍不显示该操作直接调用也返回media_type.not_mixed。测试 tests/test_comic_media_api.py 覆盖了“仅 EPUB 书籍设置 comic 返回media_type.not_mixed”“成功后media_type_lockedTrue且可再切换”等场景。四、页面 manifestGET /api/book/:bookId/comic/pagesGET /api/book/:bookId/comic/pages成功响应contract_version为 1{ err: ok, contract_version: 1, book_id: 42, title: 示例漫画, format: CBZ, revision: 9f6b69e617ec75d870c4, pages_count: 2, pages: [ { id: 9f6b69e617ec75d870c4:0, index: 0, url: /api/book/42/comic/pages/0?revision9f6b69e617ec75d870c4tokensigned-page-token, width: 1200, height: 1800, mime_type: image/jpeg } ] }字段语义index是自然排序后的连续零基序号。排序键natural_page_sort_key做了 NFKC 归一化与大小写折叠使第1页.png、第01页.png、第2页.png、第页.png按直觉顺序排列测试见 tests/test_comic_reader.pyid为{revision}:{index}格式在同一容器修订内稳定适合保存进度客户端仍应保存pageIndex作为修订变化后的回退revision是不包含路径信息的内容目录摘要由_revision对格式名 每个条目的名称、字节数与校验和做 SHA-256 后取前 20 个十六进制字符见 webserver/services/comic_archive.py。文件元数据一旦变化revision 即变化旧图片请求返回 409。逻辑错误沿用 Talebook JSON 信封{err: ..., msg: ...}HTTP 状态为 200客户端必须检查err。稳定错误码包括user.need_logincomic.book_not_foundcomic.no_permission/comic.account_inactivecomic.media_type/comic.container_missingcomic.invalid_container/comic.emptycomic.page_size/comic.page_type/comic.page_dimensions/comic.page_corruptcomic.busy。错误消息不会包含本地文件路径或归档条目名——ComicArchiveError的构造即被设计为“稳定的、无路径的错误”见 webserver/services/comic_archive.py测试也断言错误消息不含目录名与条目名。4.1 manifest 的构建与私有索引manifest 由ComicArchiveService.get_manifest构建webserver/services/comic_archive.py先通过analyze_media_file做导入级安全分析复用 webserver/services/media_analysis.py 的InvalidMediaError再列条目、过滤忽略项.DS_Store、Thumbs.db、ComicInfo.xml、__MACOSX、._*等、拒绝重复路径、做自然排序为每个条目读取头部最多MAX_COMIC_PAGE_HEADER_BYTES 2 MiB并校验字节数、魔数识别的 MIME 与扩展名匹配、Pillow 安全解码尺寸单边不超过 32768 像素、总像素不超过 1 亿超过即抛DecompressionBombWarning判定为comic.page_dimensions。manifest 使用最多 32 个文件修订的进程内 LRU 缓存COMIC_MANIFEST_CACHE_SIZE 32缓存键基于realpath 格式 st_dev/st_ino/st_size/st_mtime_ns文件变化自动失效重建。五、页面图片GET /api/book/:bookId/comic/pages/:indexGET /api/book/:bookId/comic/pages/:index?revision:revisiontoken:signedPageToken客户端只能提交数字页序、manifest 返回的不透明修订、以及原样返回的token。服务端通过私有索引解析真实条目即用page.entry_name定位并在每次响应前复核 MIME、字节数和图片完整性。5.1 页面级访问凭证signed page token页面 token 由服务端cookie_secret签名tornado.web.create_signed_value有效期1 天载荷绑定签发用户 ID、书籍 ID、页序与归档修订见 webserver/handlers/comic.pypayload tornado.escape.json_encode({ book_id: int(book_id), page_index: int(page_index), revision: revision, user_id: principal.id, }) token tornado.web.create_signed_value( str(self.settings[cookie_secret]), COMIC_PAGE_TOKEN_NAME, # comic-page-v1 payload, )因此 token不能换页、换书或修改修订后复用校验时逐项比对并重新按user_id加载用户page_token_user。请求若已携带有效登录态可不依赖 token。token 过期、篡改、用户被删除或用户后来失去阅读/私有书访问权限时接口返回 401/403/404不会继续输出图片。该机制用于 Rulia 等插件运行时插件可以用用户配置中的账号密码获取 manifest而图片加载器无需再次暴露账号密码。测试 tests/test_comic_reader.py 验证了“token 换页序后返回 401”“权限撤销后返回 403”。5.2 响应头与协议错误状态成功响应设置见 webserver/handlers/comic.pyContent-Type: image/* Content-Length: ... Cache-Control: private, max-age3600, immutable Vary: Cookie X-Content-Type-Options: nosniff Content-Security-Policy: default-src none; sandbox其中Vary: Cookie防止带认证的私有图片被公共缓存复用。协议错误使用稳定 HTTP 状态write_protocol_error输出纯文本、no-store状态码含义401未登录 / token 无效403无阅读权限 / 账号未激活404书籍或页序不存在409manifest 已更新修订过期或图片尺寸变化422容器或页面无效503并发繁忙响应正文只含可展示的简短说明绝不含路径。六、漫画进度同步GET /api/book/:bookId/comic/progress POST /api/book/:bookId/comic/progress Content-Type: application/jsonPOST 请求体{ progress: { kind: comic, version: 1, pageId: 9f6b69e617ec75d870c4:0, pageIndex: 0, percent: 50, completed: false } }服务端行为见 webserver/handlers/comic.pynormalized_progress要求kind comic且version COMIC_PROGRESS_VERSION (1)并把pageIndex钳制到[0, len(pages)-1]随后根据当前总页数重新计算percent round((page_index 1) * 100 / pages_count, 2)与completed末页为 True客户端提交的pageId必须与当前 manifest 在该pageIndex下的 ID 一致否则返回comic.progress_stale“漫画页面列表已更新请刷新阅读器”非法形状或超过2 KiBMAX_COMIC_PROGRESS_BYTES 2048的负载返回comic.progress_invalid数据复用 Talebook 的ReadingState.progress列但契约与通用 EPUB/其他阅读器相互独立保存时还会调用set_online_read(True)标记在线阅读。GET 响应除progress外还包含update_timestate.progress_update_time的 ISO 格式。前端 host 的进度策略值得参考webserver/resources/book/comic-reader.html翻页后 350ms 防抖合并保存queueProgress离页时用navigator.sendBeacon兜底保存失败则弹出“阅读进度暂未保存将在后续翻页时重试”的提示且离开阅读器不被进度保存失败阻塞。七、安全与资源预算导入与读取都会校验容器。当前边界常量集中在 webserver/services/comic_archive.py维度上限归档条目数最多 10,000 个归档展开体积最多 512 MiB导入检查单条目导入检查最多 128 MiB压缩比最多 200在线阅读单页最多 32 MiBMAX_COMIC_PAGE_BYTES另设 2 MiB 头部读取上限页面访问 token最长有效 1 天按用户、书籍、页序和归档修订隔离不包含账号密码图片尺寸单边最多 32,768 像素总像素最多 100,000,000并发读取最多 4 个并发归档读取单归档串行读取64 槽哈希锁等待 5 秒后返回 503comic.busy同时拒绝路径穿越../outside.png、重复路径、符号链接、加密归档、分卷、ZIP64、签名/扩展不匹配和损坏页面。RAR4/RAR5 由rarfile建立目录测试样例见 tests/cases/comics 下的images-rar4.rar与encrypted.cbz镜像内unar只负责解压选中的私有索引条目不能绕过应用校验。阅读时每次响应前的复核链read_pagewebserver/services/comic_archive.py为修订比对不一致 409→ 页序范围越界 404→ 解压并核对字节数与 32 MiB 上限不符comic.page_corrupt→ 头部魔数复核 MIME不符comic.page_type→ 尺寸复核变化即 409→ Pillowimage.verify()完整性验证。测试 tests/test_comic_reader.py 进一步确认陈旧修订与越界页码永远不会选中条目且所有错误消息都不含路径或条目名。八、契约兼容与版本演进本契约的所有细节——API、安全预算、进度格式——不随分发方式变化。当前阶段不发布 npm 或 release首次上游 GitHub release 产生后可再接入与 Candle Reader 相同的 release/dispatch 自动更新流程届时只需更新komga-reader-version.txt与后端KOMGA_READER_VERSION常量即可完成整站缓存失效。对二次开发者而言遵守“客户端只提交数字页序 不透明修订 原样 token”这一铁律即可安全对接任何后续版本的阅读器。参考文件索引契约文档document/ComicReaderApi.zh_CN.md后端路由与授权webserver/handlers/comic.py容器解析与资源预算webserver/services/comic_archive.py媒体类型手动设置webserver/handlers/book.py前端 host 模板webserver/resources/book/comic-reader.html静态 Reader 产物app/public/static/komga-reader版本记录komga-reader-version.txtNginx 直通配置conf/nginx/talebook.conf契约测试tests/test_comic_reader.py、tests/test_comic_media_api.py赞分享后端前端CMS【免费下载链接】talebook一个简单好用的个人书库项目地址https://gitcode.com/gh_mirrors/ta/talebook点击查看免费下载相关推荐Mihon漫画阅读器权限安全体系从安装到阅读的完整保护指南Mihon漫画阅读器权限安全体系从安装到阅读的完整保护指南 Mihon作为一款免费的安卓开源漫画阅读器在提供丰富阅读体验的同时构建了完善的权限安全体系来保移动开发Cimoc漫画阅读器从入门到精通的完整使用手册Cimoc漫画阅读器从入门到精通的完整使用手册 在数字化阅读日益普及的今天如何高效管理个人漫画收藏成为众多漫画爱好者的共同需求。Cimoc作为一款基于And移动开发上一篇NS-USBLoader完整指南Switch游戏管理的终极解决方案下一篇Spinning Up 深度强化学习教程Deep Deterministic Policy GradientDDPG原理与实战解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考