From 7bff77eaf823e12886bd6b97e7b49cd4d48ebfea Mon Sep 17 00:00:00 2001 From: hectorqin Date: Thu, 24 Sep 2026 07:47:32 +0800 Subject: [PATCH] feat(reader): deliver P0-P2 reading tools and ecosystem integrations Add reliable reading progress, verified backup and restore, reading tools, offline downloads, source quality checks, personal credential status, OPDS serving and verified WebDAV backup uploads. --- .gitignore | 2 + docker-compose.build.yml | 8 + docker-compose.yml | 9 +- docs/README.md | 8 + docs/api.md | 19 + docs/backup.zh-CN.md | 70 + docs/closed-issues-2026-09-23/issues.json | 1113 ++++++++++ docs/closed-issues-2026-09-23/issues.md | 1877 +++++++++++++++++ .../recent-comments.json | 26 + docs/closed-issues-analysis-2026-09-23.md | 179 ++ docs/development.zh-CN.md | 2 +- docs/ecosystem-roadmap.zh-CN.md | 37 + docs/getting-started.en.md | 18 +- docs/getting-started.zh-CN.md | 18 +- docs/opds-webdav.zh-CN.md | 45 + docs/p0-progress-2026-09-23.md | 26 + docs/p1-progress-2026-09-23.md | 40 + docs/p2-progress-2026-09-24.md | 50 + docs/source-plugins.md | 12 +- docs/user-guide.zh-CN.md | 21 +- server/src/config/index.ts | 3 + server/src/db/schema.ts | 19 + server/src/http/app.ts | 4 + server/src/http/routes/opds.ts | 94 + server/src/http/routes/reading-overrides.ts | 19 + server/src/http/routes/sources.ts | 9 + server/src/http/routes/tts.ts | 12 + server/src/indexer/formats/epub.ts | 30 +- server/src/indexer/formats/navigation.ts | 55 + server/src/maintenance/backup.ts | 138 ++ server/src/maintenance/cli.ts | 30 + server/src/maintenance/webdav-cli.ts | 19 + server/src/maintenance/webdav.ts | 105 + server/src/publications/chapter-quality.ts | 19 + server/src/publications/chapters.ts | 18 + server/src/services/opds.ts | 45 + server/src/services/reading-overrides.ts | 33 + server/src/services/shelf.ts | 6 + server/src/services/source-access.ts | 43 + server/src/services/source-host.ts | 51 +- server/src/services/sync.ts | 6 +- server/src/services/tts.ts | 39 +- server/test/backup.test.ts | 79 + server/test/chapter-publications.test.ts | 23 + server/test/chapter-quality.test.ts | 12 + server/test/config.test.ts | 14 +- server/test/library-scale.test.ts | 27 + server/test/opds-server.test.ts | 93 + server/test/reading-overrides.test.ts | 35 + server/test/sources-api.test.ts | 49 +- server/test/tts.test.ts | 21 +- server/test/webdav-backup.test.ts | 62 + web/package-lock.json | 35 + web/package.json | 1 + web/src/api/client.ts | 20 +- web/src/api/errors.ts | 2 +- web/src/api/sources.ts | 4 + web/src/api/types.ts | 2 + web/src/app.ts | 5 +- web/src/core/offline-download.ts | 85 + web/src/formats/epub.ts | 45 +- web/src/formats/navigation.ts | 55 + web/src/formats/txt.ts | 4 +- web/src/store/offline.ts | 4 +- web/src/store/publications.ts | 74 +- web/src/styles/reader.css | 34 +- web/src/ui/opds-access.tsx | 52 + web/src/ui/reader-chrome.tsx | 40 +- web/src/ui/reader-screen.tsx | 336 ++- web/src/ui/reader-view.ts | 62 + web/src/ui/reading-overrides.ts | 15 + web/src/ui/reading-tools.tsx | 142 ++ web/src/ui/source-capabilities.tsx | 17 + web/src/ui/sources-screen.tsx | 34 +- web/src/ui/text-anchor.ts | 83 + web/test/api-account-switch.test.ts | 6 +- web/test/chapter-reader.test.ts | 95 +- web/test/reading-tools-core.test.ts | 101 + web/test/sources-screen.test.ts | 48 +- web/test/sync.test.ts | 13 + web/tools/ui-review/p1-reading.mjs | 52 + web/tools/ui-review/p2-sources.mjs | 66 + 82 files changed, 6158 insertions(+), 166 deletions(-) create mode 100644 docker-compose.build.yml create mode 100644 docs/backup.zh-CN.md create mode 100644 docs/closed-issues-2026-09-23/issues.json create mode 100644 docs/closed-issues-2026-09-23/issues.md create mode 100644 docs/closed-issues-2026-09-23/recent-comments.json create mode 100644 docs/closed-issues-analysis-2026-09-23.md create mode 100644 docs/ecosystem-roadmap.zh-CN.md create mode 100644 docs/opds-webdav.zh-CN.md create mode 100644 docs/p0-progress-2026-09-23.md create mode 100644 docs/p1-progress-2026-09-23.md create mode 100644 docs/p2-progress-2026-09-24.md create mode 100644 server/src/http/routes/opds.ts create mode 100644 server/src/http/routes/reading-overrides.ts create mode 100644 server/src/indexer/formats/navigation.ts create mode 100644 server/src/maintenance/backup.ts create mode 100644 server/src/maintenance/cli.ts create mode 100644 server/src/maintenance/webdav-cli.ts create mode 100644 server/src/maintenance/webdav.ts create mode 100644 server/src/publications/chapter-quality.ts create mode 100644 server/src/services/opds.ts create mode 100644 server/src/services/reading-overrides.ts create mode 100644 server/src/services/source-access.ts create mode 100644 server/test/backup.test.ts create mode 100644 server/test/chapter-quality.test.ts create mode 100644 server/test/library-scale.test.ts create mode 100644 server/test/opds-server.test.ts create mode 100644 server/test/reading-overrides.test.ts create mode 100644 server/test/webdav-backup.test.ts create mode 100644 web/src/core/offline-download.ts create mode 100644 web/src/formats/navigation.ts create mode 100644 web/src/ui/opds-access.tsx create mode 100644 web/src/ui/reading-overrides.ts create mode 100644 web/src/ui/reading-tools.tsx create mode 100644 web/src/ui/source-capabilities.tsx create mode 100644 web/src/ui/text-anchor.ts create mode 100644 web/test/reading-tools-core.test.ts create mode 100644 web/tools/ui-review/p1-reading.mjs create mode 100644 web/tools/ui-review/p2-sources.mjs diff --git a/.gitignore b/.gitignore index 9dac853..e927b21 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ web/dist/ # reviewer reads, and the part CI asserts on. docs/ui-review/*.png docs/ui-review/mobile/*.png +docs/ui-review/p1/*.png +docs/ui-review/p2/*.png diff --git a/docker-compose.build.yml b/docker-compose.build.yml new file mode 100644 index 0000000..d539750 --- /dev/null +++ b/docker-compose.build.yml @@ -0,0 +1,8 @@ +# Use with: docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build +services: + reader: + image: reader:local + pull_policy: never + build: + context: . + dockerfile: server/Dockerfile diff --git a/docker-compose.yml b/docker-compose.yml index dd459c4..17c40da 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,9 @@ # automatically becomes the instance admin. services: reader: - image: cnb.cool/hectorqin/reader:latest + # CI publishes branch and commit tags; it does not publish :latest. + # Pin READER_IMAGE to a published commit tag for repeatable upgrades. + image: ${READER_IMAGE:-cnb.cool/hectorqin/reader:main} # To build from source instead of pulling the published image. The context is # the repository root because the image bakes in the H5 client built from # ./web as well as the server. @@ -15,6 +17,11 @@ services: # dockerfile: server/Dockerfile container_name: reader restart: unless-stopped + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" ports: - "8080:8080" environment: diff --git a/docs/README.md b/docs/README.md index b7a3ad6..3900bac 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,6 +13,7 @@ The project README introduces reader. Use the guides below for installation, eve | 书架、阅读、朗读、来源和离线 / Shelves, reading, TTS, sources, offline | [使用手册](user-guide.zh-CN.md) | Chinese only | | 安装到桌面、离线启动和版本更新 / PWA installation, offline startup and updates | [PWA 指南](pwa.zh-CN.md) | Chinese only | | 环境变量、目录权限、HTTPS、排错 / Settings, permissions, HTTPS, troubleshooting | [部署配置](configuration.zh-CN.md) | [Configuration](configuration.en.md) | +| 数据备份、校验与恢复 / Data backup, verification and restore | [备份恢复](backup.zh-CN.md) | Chinese only | | 本地开发、验证、镜像与 Android 构建 / Development, checks, Docker and Android builds | [开发指南](development.zh-CN.md) | [Development](development.en.md) | ## 技术参考 · Technical reference @@ -36,3 +37,10 @@ These records describe checks at specific delivery points. Use tests and CI for - [书源界面 / Source UI](ui-review/sources-ux.md) - [书库与阅读界面 / Library and reader UI](ui-review/collections-ux.md) - [书源搜索 / Source search](ui-review/source-search.md) + +- [P1 阅读工具实施与验证](p1-progress-2026-09-23.md) + +- [P2 交付与验证记录](p2-progress-2026-09-24.md) +- [生态互通与客户端覆盖方案](ecosystem-roadmap.zh-CN.md) + +- [OPDS 服务端接入与 WebDAV 备份上传](opds-webdav.zh-CN.md) diff --git a/docs/api.md b/docs/api.md index 22e5bb8..e9fa25e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1112,3 +1112,22 @@ Invoke-RestMethod -Method Post -Uri "$readerApi/books/$readerBookId/refresh" -He - `POST /api/v1/books/:id/switch-source`:`{entryRef,chapterId,revision}` → `{content,href}`;所选正文验证成功后事务切换,保留 bookId。目录变更返回冲突,客户端刷新再选择。 后台任务 RPC、持久化及限制见[插件扩展设计](plugin-extensions.md)。页面只接受声明式数据,不执行插件提供的浏览器脚本。 + +## Personal reading overrides and TTS diagnostics + +All endpoints require the account access token in the Authorization header. Reading overrides additionally check book access. + +- GET /api/v1/books/:id/reading-overrides returns { version, headingPrefix, corrections }. +- PUT /api/v1/books/:id/reading-overrides accepts the same object. A correction contains id, anchor (sectionId, start, end, quote, prefix, suffix), and replacement. The submitted version must match the latest version; stale writes return 409. Up to 500 corrections are allowed, with each quote/replacement at most 4,000 characters. headingPrefix is a literal prefix of at most 60 characters. +- POST /api/v1/books/:id/reading-overrides/undo accepts { version } and saves the preceding snapshot as a new version. History is scoped to account/book and stored in DATA_DIR/reader.db; it never rewrites library files. +- POST /api/v1/tts/test accepts { voice?, speed? }, synthesizes a fixed sample and returns { bytes, contentType, elapsedMs, cached }. Failure codes include TTS_DISABLED, TTS_AUTH, TTS_TIMEOUT, TTS_CONTENT, TTS_EMPTY and TTS_UPSTREAM. It does not accept an upstream URL. Audio playback remains GET /api/v1/tts. + +### 章节质量和个人凭据状态(P2) + +- `POST /api/v1/books/:id/switch-quality`:已登录且拥有该书,body 为 `{ entryRef, chapterId }`。沿当前来源插件获取候选正文,返回 `{ chapterId, title, latestChapter, characters, images, bytes, elapsedMs }`;不写入书架、绑定或正文缓存。耗时包含目录、正文、内嵌图片处理,不含前置 acquire 和网络往返。 +- `switch-preview` 额外返回 `latestChapter`。质量检测按需执行;确认换源时重新获取并验证正文。空白或清洗后为空的正文返回 `EMPTY_CHAPTER`,不覆盖旧缓存/绑定。图片章可通过,字符统计不包含空白与 HTML 标签。 +- `GET /api/v1/sources/:id/credentials`:仅当前账号的 `{ state, checkedAt, available, fields: [{ key, label, configured }] }`,不包含凭据值。state 为 unknown / reachable / auth-required / verification-required。checkedAt 是毫秒时间戳或 null;仅代表最近观察。 +- `DELETE /api/v1/sources/:id/credentials/:key`:只删除当前账号该项。原 PUT 保存空字符串也会删除;保存/删除后访问状态回到 unknown。修改连接配置会清除该来源所有个人凭据并重置状态。 +- 插件抛出 `AUTH_REQUIRED`、`AUTH_EXPIRED` 或 `VERIFICATION_REQUIRED` 时记录访问状态;它们不使 Reader 账号退出。部分搜索错误中出现同类码也会记录;多个站点共用一个实例时只表示其中有站点需要处理。普通网络失败不推断为凭据失效。 + +- [OPDS 服务端接入与 WebDAV 备份上传](opds-webdav.zh-CN.md) diff --git a/docs/backup.zh-CN.md b/docs/backup.zh-CN.md new file mode 100644 index 0000000..152cb46 --- /dev/null +++ b/docs/backup.zh-CN.md @@ -0,0 +1,70 @@ +# 数据备份与恢复 + +维护命令随服务端构建发布,覆盖整个 `DATA_DIR`:SQLite(含 WAL)、账号、书架、进度、笔记、签名密钥、插件包/状态、下载与缓存。SQLite 的共享内存文件会重新生成,不计入备份。**`BOOKS_DIR` 原书、Compose 配置和环境变量需另行备份。** 如果签名密钥由环境变量提供,也需保留原配置。 + +这是离线维护工具。运行 backup/restore 前停止所有使用该数据目录的服务和插件;`--server-stopped` 是操作者确认,不会自动检测或停止服务。校验和能发现意外损坏,不证明备份来自可信来源。 + +## 创建和校验 + +源码方式先在 `server` 运行 `npm run build`,之后执行: + +```sh +node server/dist/maintenance/cli.js backup /absolute/data /absolute/backups/reader-20260923 --server-stopped +node server/dist/maintenance/cli.js verify /absolute/backups/reader-20260923 +``` + +目标目录必须不存在,父目录必须已存在;不能与数据目录重叠。工具逐文件复制并计算 SHA-256,复制后再次核对源/目标,并检查 SQLite 完整性。不会覆盖旧备份。 + +Docker Compose(以下为 Linux/macOS shell;先确保当前镜像包含维护命令): + +```sh +mkdir -p ./backups +docker compose stop reader +docker compose run --rm --no-deps --user 0:0 --entrypoint node \ + -v "$(pwd)/backups:/backups" reader \ + dist/maintenance/cli.js backup /data /backups/reader-20260923 --server-stopped +docker compose run --rm --no-deps --user 0:0 --entrypoint node \ + -v "$(pwd)/backups:/backups" reader \ + dist/maintenance/cli.js verify /backups/reader-20260923 +docker compose up -d reader +``` + +如果备份/校验失败,先处理错误,不要继续升级。只执行 `verify` 可以在原服务运行时检查独立的备份目录,但不要同时修改备份。 + +Windows 源码命令可使用 `"D:/reader/data"` 等绝对路径。Docker bind mount 用绝对路径替换 `$(pwd)`。 + +## 恢复到新目录 + +先验证备份,再恢复到尚不存在的新数据目录: + +```sh +node server/dist/maintenance/cli.js restore /absolute/backups/reader-20260923 /absolute/data-restored --server-stopped +``` + +Docker 示例使用独立临时容器,不挂载旧数据目录。`READER_IMAGE` 替换为含维护命令的实际镜像标签: + +```sh +docker compose stop reader +docker run --rm --user 0:0 --entrypoint node \ + -v "$(pwd)/backups:/backups" \ + -v "$(pwd):/restore" READER_IMAGE \ + dist/maintenance/cli.js restore /backups/reader-20260923 /restore/data-restored --server-stopped +# 上面的维护容器以 root 写入;只调整新恢复目录的所有权。 +docker run --rm --user 0:0 --entrypoint chown \ + -v "$(pwd)/data-restored:/data" READER_IMAGE -R reader:reader /data +``` + +恢复成功且完成上述所有权调整后,把 Compose 的数据挂载改成 `./data-restored:/data`,保留原书挂载和环境变量,再启动同一版本服务。正常启动入口只处理 `/data` 根目录,不会递归修复恢复文件的所有权;源码运行时同样需保证运行账号可读写整个恢复目录。 + +验收:原账号可登录;书架、进度、笔记和来源配置正确;已下载内容可读。确认前保留旧数据目录,需要回退时停止服务并切回旧挂载。 + +## 失败与限制 + +- 失败目标保留 `.reader-backup-incomplete`,服务拒绝从该目录启动;不要手动删除标记绕过校验,应修复原因后重新操作到新目录。 +- `reader-backup.json` 是完成备份的文件清单;不能直接把备份目录作为运行数据目录,必须执行 restore。 +- 仅支持当前 Node 版 reader 数据库;不转换旧 Java 版 `/storage` 数据。 +- 保留目录内部的相对符号链接;拒绝绝对链接、外部链接和特殊设备文件。Windows 创建符号链接可能需要开发者模式或相应权限。 +- 本工具不保证在服务运行中复制的一致性;停机是使用前提。书籍多时备份可能较大,请确保目标空间充足。 +- 备份含账号及插件凭据,应使用受限权限存放。 + +- [OPDS 服务端接入与 WebDAV 备份上传](opds-webdav.zh-CN.md) diff --git a/docs/closed-issues-2026-09-23/issues.json b/docs/closed-issues-2026-09-23/issues.json new file mode 100644 index 0000000..0d293b1 --- /dev/null +++ b/docs/closed-issues-2026-09-23/issues.json @@ -0,0 +1,1113 @@ +[ + { + "number": 718, + "title": "会考虑增加书源登陆的功能吗?", + "html_url": "https://github.com/hectorqin/reader/issues/718", + "created_at": "2026-06-06T13:42:08Z", + "closed_at": "2026-09-23T09:25:14Z", + "state_reason": "not_planned", + "comments": 0, + "body": "现在好的书源都要会员和登陆才行。", + "labels": null + }, + { + "number": 717, + "title": "希望可以配置导出epub在nas里也存一份,并且可以设置自定义保存目录,另外希望能支持opds", + "html_url": "https://github.com/hectorqin/reader/issues/717", + "created_at": "2026-05-27T15:37:20Z", + "closed_at": "2026-09-23T09:25:13Z", + "state_reason": "not_planned", + "comments": 0, + "body": null, + "labels": null + }, + { + "number": 716, + "title": "【功能请求】:内容相似度显示", + "html_url": "https://github.com/hectorqin/reader/issues/716", + "created_at": "2026-05-19T02:06:39Z", + "closed_at": "2026-09-23T09:25:13Z", + "state_reason": "not_planned", + "comments": 0, + "body": "大佬好,请问能否出一个功能,当切换书源的时候,在书源每行的延迟时间旁边添加两个内容,一个是该书源在本章的字数,另一个是该书源在和本书源在本章节的相似度(可以直接借用notepad++的compare功能)。\n这样如果在读书的时候,这个书源的内容被修改了,可以快速的找到正确的书源内容,不用再一个一个更换了", + "labels": null + }, + { + "number": 712, + "title": "(建议)对于arm版Windows的构建", + "html_url": "https://github.com/hectorqin/reader/issues/712", + "created_at": "2026-02-02T14:50:50Z", + "closed_at": "2026-09-23T09:25:12Z", + "state_reason": "not_planned", + "comments": 0, + "body": "希望可以构建Windows的arm版本\n\n", + "labels": null + }, + { + "number": 714, + "title": "Docker上用什么字段来修改默认书源上限?", + "html_url": "https://github.com/hectorqin/reader/issues/714", + "created_at": "2026-04-20T01:50:37Z", + "closed_at": "2026-09-23T09:25:12Z", + "state_reason": "not_planned", + "comments": 1, + "body": "defaultUserBookSourceLimit修改到2000是无效的,最高100。用什么字段来修改默认书源上限?", + "labels": null + }, + { + "number": 711, + "title": "【功能建议】可以自定义网页标题", + "html_url": "https://github.com/hectorqin/reader/issues/711", + "created_at": "2026-01-10T01:04:00Z", + "closed_at": "2026-09-23T09:25:11Z", + "state_reason": "not_planned", + "comments": 1, + "body": "希望可以自定义网页标题,现在默认是小说名字+章节,如果能自定义就能更隐蔽的阅读。", + "labels": null + }, + { + "number": 710, + "title": "【功能建议】多级目录", + "html_url": "https://github.com/hectorqin/reader/issues/710", + "created_at": "2026-01-06T16:23:25Z", + "closed_at": "2026-09-23T09:25:11Z", + "state_reason": "not_planned", + "comments": 0, + "body": "想要多级目录", + "labels": null + }, + { + "number": 709, + "title": "【功能申请】内容划线与注释", + "html_url": "https://github.com/hectorqin/reader/issues/709", + "created_at": "2026-01-06T15:33:57Z", + "closed_at": "2026-09-23T09:25:10Z", + "state_reason": "not_planned", + "comments": 0, + "body": "想要这个功能,可能对文字内容进行划线,并可以选择对其进行文字注释。用的 docker 版本,没找到这种操作,不知道桌面端有没有", + "labels": null + }, + { + "number": 707, + "title": "会创造大量docker日志,填满磁盘", + "html_url": "https://github.com/hectorqin/reader/issues/707", + "created_at": "2025-12-26T06:31:58Z", + "closed_at": "2026-09-23T09:25:10Z", + "state_reason": "not_planned", + "comments": 1, + "body": "每天都会创建大量日志,导致磁盘被空间", + "labels": null + }, + { + "number": 706, + "title": "com.jayway.jsonpath.PathNotFoundException: No results for path: $['data']", + "html_url": "https://github.com/hectorqin/reader/issues/706", + "created_at": "2025-12-25T16:43:35Z", + "closed_at": "2026-09-23T09:25:09Z", + "state_reason": "not_planned", + "comments": 0, + "body": "不管什么书源加入书全是这个报错\ncom.jayway.jsonpath.PathNotFoundException: No results for path: $['data']", + "labels": null + }, + { + "number": 703, + "title": "能否集成cloudscraper 实现自动过cf盾?", + "html_url": "https://github.com/hectorqin/reader/issues/703", + "created_at": "2025-12-08T13:11:57Z", + "closed_at": "2026-09-23T09:25:09Z", + "state_reason": "not_planned", + "comments": 0, + "body": "能否集成 https://github.com/VeNoMouS/cloudscraper ,实现自动过书源站点的cf盾,例如https://www.69shuba.com/ 这种优质书源站点挂了cf盾,不能自动过盾的话,就没法搜索阅读。\n谢谢!", + "labels": null + }, + { + "number": 704, + "title": "浏览书仓,是否可以搜索子目录?", + "html_url": "https://github.com/hectorqin/reader/issues/704", + "created_at": "2025-12-16T13:47:58Z", + "closed_at": "2026-09-23T09:25:09Z", + "state_reason": "not_planned", + "comments": 0, + "body": "我的本地书仓按照书籍类型有几个子目录,导入本地书仓时,搜索功能无法搜索子目录", + "labels": null + }, + { + "number": 702, + "title": "能够自动加载下一章,每次都得手动点击下一章,特别影响阅读体验", + "html_url": "https://github.com/hectorqin/reader/issues/702", + "created_at": "2025-12-08T01:14:22Z", + "closed_at": "2026-09-23T09:25:08Z", + "state_reason": "not_planned", + "comments": 3, + "body": "我是nas部署的,然后用手机浏览器阅读,每次音量键翻页到章节末页都得手动点击下一章,特别影响阅读体验", + "labels": null + }, + { + "number": 700, + "title": "能否适配阅读客户端的webdav同步逻辑", + "html_url": "https://github.com/hectorqin/reader/issues/700", + "created_at": "2025-11-15T05:03:21Z", + "closed_at": "2026-09-23T09:25:08Z", + "state_reason": "not_planned", + "comments": 0, + "body": "实现自动同步,而非需要手动点击还原", + "labels": null + }, + { + "number": 699, + "title": "有办法把其他平台的评论抓过来吗", + "html_url": "https://github.com/hectorqin/reader/issues/699", + "created_at": "2025-11-08T09:48:14Z", + "closed_at": "2026-09-23T09:25:07Z", + "state_reason": "not_planned", + "comments": 0, + "body": "如题", + "labels": null + }, + { + "number": 698, + "title": "能自动下一章吗?", + "html_url": "https://github.com/hectorqin/reader/issues/698", + "created_at": "2025-11-06T11:29:25Z", + "closed_at": "2026-09-23T09:25:07Z", + "state_reason": "not_planned", + "comments": 0, + "body": "每一章读到最后,能自动加载下一章吗?不需要手动点击才能下一章。", + "labels": null + }, + { + "number": 696, + "title": "跳段很影响观感", + "html_url": "https://github.com/hectorqin/reader/issues/696", + "created_at": "2025-10-17T10:18:49Z", + "closed_at": "2026-09-23T09:25:06Z", + "state_reason": "not_planned", + "comments": 0, + "body": "把跳段修修吧,求你了很影响看书", + "labels": null + }, + { + "number": 697, + "title": "导入书源过多,直接崩溃,重新打开,书架的书,一直显示报错", + "html_url": "https://github.com/hectorqin/reader/issues/697", + "created_at": "2025-11-03T03:01:47Z", + "closed_at": "2026-09-23T09:25:06Z", + "state_reason": "not_planned", + "comments": 1, + "body": "一直出现下面的报错,io.vertx.core.json.DecodeException: Failed to decode:No content to map due to end-of-input at [Source: (String)\"\"; line: 1, column: 0]", + "labels": null + }, + { + "number": 694, + "title": "【功能建议】建议增加AI速读功能", + "html_url": "https://github.com/hectorqin/reader/issues/694", + "created_at": "2025-09-11T14:02:44Z", + "closed_at": "2026-09-23T09:25:05Z", + "state_reason": "not_planned", + "comments": 0, + "body": "建议增加AI总结功能,可通过大模型API对每章内容进行总结提炼,帮助快速阅读", + "labels": null + }, + { + "number": 695, + "title": "依赖出现了CVE,建议升级下对应依赖版本", + "html_url": "https://github.com/hectorqin/reader/issues/695", + "created_at": "2025-10-14T19:02:18Z", + "closed_at": "2026-09-23T09:25:05Z", + "state_reason": "not_planned", + "comments": 0, + "body": "**Describe the bug 描述你遇到的问题**\nFasterXML jackson-databind远程代码执行漏洞(CVE-2019-17531)\n在3.2.14 jar包中扫描出了有问题版本的依赖,建议升级下依赖版本。\n", + "labels": null + }, + { + "number": 693, + "title": "请教如何把reader3服务器做成书源导入阅读apk?", + "html_url": "https://github.com/hectorqin/reader/issues/693", + "created_at": "2025-08-28T11:36:02Z", + "closed_at": "2026-09-23T09:25:05Z", + "state_reason": "not_planned", + "comments": 7, + "body": "如标题所述,起因需求是reader3服务器能正常访问海外,而手机毕竟无法长期上T,如果能把服务器做成书源导入手机阅读apk,那不就方便了么?\n请高手指点一下如何操作?", + "labels": null + }, + { + "number": 692, + "title": "书籍内搜索无法定位", + "html_url": "https://github.com/hectorqin/reader/issues/692", + "created_at": "2025-08-24T08:22:09Z", + "closed_at": "2026-09-23T09:25:04Z", + "state_reason": "not_planned", + "comments": 0, + "body": "作者您好,我在使用这个软件阅读时,我在书籍内搜索关键字,会列出所有结果,我点击一个结果,然后只会跳转到结果所在的那一章的开头而不是到这个结果的具体位置。我尝试了使用单页模式(切换成手机模式再用左右翻面)还是一样的情况。是我没有设置对还是这是一个bug?", + "labels": null + }, + { + "number": 691, + "title": "【功能申请】能否在不同的客户端能使用不同的主题?", + "html_url": "https://github.com/hectorqin/reader/issues/691", + "created_at": "2025-08-23T08:42:14Z", + "closed_at": "2026-09-23T09:25:04Z", + "state_reason": "not_planned", + "comments": 0, + "body": "不同的设备要使用不同的主题,电脑,手机,电纸书(还分彩色和黑白)等等要使用不同的主题才更适合。\n\n客户端做本地标识,然后再后台列表记录一下应该就可以了。或者直接把主题选择存在本地更简单。\n\n\n\n另外,还有个问题,打开页面时读书籍列表很废时,体验不好,能不能在本地缓存之前的,打开时先显示缓存,更新后再前台刷新,这样体验应该会更好。", + "labels": null + }, + { + "number": 690, + "title": "[web] 朗读功能失效", + "html_url": "https://github.com/hectorqin/reader/issues/690", + "created_at": "2025-08-20T04:59:48Z", + "closed_at": "2026-09-23T09:25:03Z", + "state_reason": "not_planned", + "comments": 4, + "body": "**为避免无效问题和冗余问题,提问前请确认**\n1. 你确定Google不能解决你的问题\n2. 你确定已有的issue不能解决你的问题\n3. 你确定issue的title按照格式如下:[web/simple-web/server]:description\n\n**Describe the bug 描述你遇到的问题**\n朗读功能突然失效, 包含`浏览器`和 `大声朗读`\n\n**To Reproduce 如何重现问题**\nSteps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容:\n1. 打开小说\n2. 点开朗读功能\n3. 任意朗读服务\n4. 点击播放\n\n**Expected behavior 期待修复的效果**\n播放朗读\n\n**Screenshots 如有必要,可以截图说明**\n-\n\n**版本说明**\n - OS: 树莓pi 4, ubuntu \n - docker 部署\n - hectorqin/reader:openj9-latest\n - chrome 浏览器(桌面端 & 手机端)\n\n**Additional context 其他说明**\n检查 api 返回内容, 并没有资料, 返回内容如下:\n```json\n{\"isSuccess\":true,\"errorMsg\":\"\",\"data\":\"\"}\n```\n", + "labels": null + }, + { + "number": 689, + "title": "听书想用第三方的语音要怎么配置啊?", + "html_url": "https://github.com/hectorqin/reader/issues/689", + "created_at": "2025-08-19T11:52:39Z", + "closed_at": "2026-09-23T09:25:02Z", + "state_reason": "not_planned", + "comments": 4, + "body": "我想用听书功能,但默认的语音太难听了,想自己配置第三方语音但不知道怎么配,这个HttpTTS是用来配置第三方的吗?文档里也没有说明。\n\n\"Image\"", + "labels": null + }, + { + "number": 688, + "title": "希望修改文章内容是在原txt修改而非重新生成", + "html_url": "https://github.com/hectorqin/reader/issues/688", + "created_at": "2025-08-13T09:18:59Z", + "closed_at": "2026-09-23T09:17:36Z", + "state_reason": "not_planned", + "comments": 1, + "body": " 目前编辑文章内容是重新生成一份新的txt,包括从书仓导入的,这样导致编辑的内容只是在前端web显示,书仓的原书籍依然是不变的。\n 并且一旦刷新更改的内容就全部消失,但是后端新建的txt又还在,这样的操作逻辑比较奇怪。\n 不清楚实现这样的功能是否过于复杂,如果难以实现的话也只能通过手动修改书仓内txt进行变更,就是比较麻烦,主要是因为部分文章存在缺文少字的情况,难免会有需要手动更改,但是目前的修改功能感觉并不能很好的支撑这一需求。\n\n\"Image\"", + "labels": null + }, + { + "number": 684, + "title": "希望可以锁定横向滑动,添加全屏", + "html_url": "https://github.com/hectorqin/reader/issues/684", + "created_at": "2025-08-05T07:42:55Z", + "closed_at": "2026-09-23T09:17:35Z", + "state_reason": "not_planned", + "comments": 0, + "body": "1. 我在使用触控板进行滑动阅读的时候,有时候会略微偏斜导致先横向滑动,后竖向滚动,发生页面晃动,影响阅读体验。我试了一下各个浏览器,发现似乎只有谷歌浏览器是横向滑动的时候页面不动,希望可以锁定横向滑动。\n\n2. 希望添加类似浏览器f11的全屏功能\n", + "labels": null + }, + { + "number": 687, + "title": "docker compose 部署", + "html_url": "https://github.com/hectorqin/reader/issues/687", + "created_at": "2025-08-12T09:27:10Z", + "closed_at": "2026-09-23T09:17:35Z", + "state_reason": "not_planned", + "comments": 3, + "body": "每次都是从第一章开始,没有获取阅读进度\n", + "labels": null + }, + { + "number": 677, + "title": "书海数据响应问题", + "html_url": "https://github.com/hectorqin/reader/issues/677", + "created_at": "2025-06-30T02:18:46Z", + "closed_at": "2026-09-23T09:17:34Z", + "state_reason": "not_planned", + "comments": 0, + "body": "**为避免无效问题和冗余问题,提问前请确认**\n1. 你确定Google不能解决你的问题\n2. 你确定已有的issue不能解决你的问题\n3. 你确定issue的title按照格式如下:[web/simple-web/server]:description\n\n**Describe the bug 描述你遇到的问题**\nA clear and concise description of what the bug is. 简洁有效的说明。\n书海哪里点击响应问题\n\n**To Reproduce 如何重现问题**\nSteps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容:\n1. Go to '...'\n2. \n\n![Image](https://github.com/user-attachments/assets/c3bbb451-fef5-434e-8daa-9077790a67cc)\n点击了 但是没有数据请求\n3. Click on '....'\n4. 点击下一个选项有了上一个的数据请求并响应回来\n\n![Image](https://github.com/user-attachments/assets/424f299c-70aa-4e0e-bec5-841cba1ac9b5)\n\n5. Scroll down to '....'\n6. See error**Expected behavior 期待修复的效果**\nA clear and concise description of what you expected to happen. 简单描述。\n\n**Screenshots 如有必要,可以截图说明**\nIf applicable, add screenshots to help explain your problem.\n\n**版本说明**\n - OS: [e.g. win] win\n - Deploy Method web版\n - Program Version 3.2.14\n - Browser [e.g. chrome, safari] edge\n\n**Additional context 其他说明**\nAdd any other context about the problem here. 添加你认为有必要的内容,否则不写。\n", + "labels": null + }, + { + "number": 679, + "title": "可以新增功能支持opds协议,来连接自己的书库吗,比如calibre-web?", + "html_url": "https://github.com/hectorqin/reader/issues/679", + "created_at": "2025-07-04T04:08:51Z", + "closed_at": "2026-09-23T09:17:34Z", + "state_reason": "not_planned", + "comments": 1, + "body": "目前自己同时也部署了一套calibre-web,希望可以通过reader3来连接calibre-web来阅读自己本地的书籍,reader3本身自带的本地书仓不太好用,默认只能放到localstorage下,挂载了其他目录也访问不到。", + "labels": null + }, + { + "number": 674, + "title": "突然无法打开书架和添加本地书籍,原账号可以登录成功,但原书架数据全都丢失?", + "html_url": "https://github.com/hectorqin/reader/issues/674", + "created_at": "2025-06-18T08:27:17Z", + "closed_at": "2026-09-23T09:17:33Z", + "state_reason": "not_planned", + "comments": 0, + "body": "**为避免无效问题和冗余问题,提问前请确认**\n1. 你确定Google不能解决你的问题\n2. 你确定已有的issue不能解决你的问题\n3. 你确定issue的title按照格式如下:[web/simple-web/server]:description\n\n**Describe the bug 描述你遇到的问题**\nA clear and concise description of what the bug is. 简洁有效的说明。\n1、我是在群晖上用docker架设的阅读服务器(多用户模式),长期使用域名访问看书起码半年多了。今天群晖突然故障重启,然后阅读服务器下我个人常用的账号书架书籍全消失了,浏览书仓功能可以进入,但要重新添加书仓内的本地txt小说却会失败,会提示:“io.vertx.core,json.DecodeException: Failed to decode:No content to map due to end-of input at [Source: ($tring)\"; line: 1, column: 0]”;该账号登录是会有提示“io.vertx.corejson.DecodeException: Failed to decode:No content to map due to end-of-input at [Source:(String)\"; line: 1, column: 0]”两次及提示“没有备份文件”。如图所示。\n奇怪的是,用其他账号登录,书架也在,添加书籍操作也在。\n2、我访问服务用的是网页版,但是在PC端chrome浏览器下访问,关键词搜索新书籍时会搜不到书,要在手机端chrome浏览器下就可以搜索到到书籍,明明是同样的书源。\n**To Reproduce 如何重现问题**\nSteps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior 期待修复的效果**\nA clear and concise description of what you expected to happen. 简单描述。\n希望咨询账号数据恢复的手段。\n**Screenshots 如有必要,可以截图说明**\nIf applicable, add screenshots to help explain your problem.\n\n![Image](https://github.com/user-attachments/assets/79959a1c-5fd1-49b4-9557-a818e0d80a60)\n\n![Image](https://github.com/user-attachments/assets/396ecdfc-e582-4f9e-a36c-c8536077eb92)\n\n**版本说明**\n - OS: 群晖DSM\n - Deploy Method docker部署\n - Program Version最新的3.2.14\n - Browser [e.g. chrome, safari] chrome浏览器,PC端\n\n**Additional context 其他说明**\nAdd any other context about the problem here. 添加你认为有必要的内容,否则不写。\n", + "labels": null + }, + { + "number": 676, + "title": "[Web] 3.2.14 版本中 “关闭书架虚拟列表优化” 也无法将单列目录还原,且关闭后当书架书籍数量不足时,会拉伸书籍div高度来自适应书架高度,造成界面排版异常", + "html_url": "https://github.com/hectorqin/reader/issues/676", + "created_at": "2025-06-28T03:14:32Z", + "closed_at": "2026-09-23T09:17:33Z", + "state_reason": "not_planned", + "comments": 13, + "body": "**Describe the bug 描述你遇到的问题**\n1、更新 `3.2.14` 版本 [reader-pro-3.2.14.jar](https://github.com/hectorqin/reader/releases/download/v3.2.14/reader-pro-3.2.14.jar) 并重新启动后,默认启用了 `书架虚拟列表优化` ,界面显示单列但无异常;\n2、在左侧 `书架设置` --> `列表优化` 中选择 `不启用优化` 后保存,因为曾经为之前的版本为书籍的 info div 块添加了 `height: auto;min-height: 112px;`,因此发现在更新错误分组中书籍高度显示异常,经检查发现即使去掉了添加的 css 样式,凡是分组中书籍数量过少的界面,书籍的 div 块高度都会默认拉伸铺满分组书架界面;\n3、即使在 “关闭书架虚拟列表优化” 后,进入书籍,目录列表显示也是单列,没有找到可以调整的设置,无法调整。试过重置后重新查看、备份后重新查看均无效。\n\n**Expected behavior 期待修复的效果**\n希望书籍目录列表可以正常切换单列和多列;书籍少的分组界面显示优化。\n\n**Screenshots 如有必要,可以截图说明**\n1、`启用虚拟列表优化` 时界面显示\n\"Image\"\n2、`不启用优化` 时书架分组界面显示\n【添加 css 样式】\n\"Image\"\n【去除 css 样式】\n\"Image\"\n3、`不启用优化` 时书籍列表界面显示\n\"Image\"\n\n**版本说明**\n - OS: Windows 11\n - Deploy Method: [reader-pro-3.2.14.jar](https://github.com/hectorqin/reader/releases/download/v3.2.14/reader-pro-3.2.14.jar)\n - Program Version: 3.2.14\n - Browser: edge 浏览器\n", + "labels": null + }, + { + "number": 672, + "title": "同样的书源手机客户端能搜到书籍,服务器版搜索不到", + "html_url": "https://github.com/hectorqin/reader/issues/672", + "created_at": "2025-06-17T01:55:40Z", + "closed_at": "2026-09-23T09:17:32Z", + "state_reason": "not_planned", + "comments": 5, + "body": "手机上用的阅读3客户端,在云服务器上也部署了3.2.14版本。手机和服务器上的书源是同一套,但是同样的小说在阅读3上能搜到,在服务器版上可能就搜不到。而且搜索结果的数量也是少很多。我会调的只有并发线程,把线程调高了也没什么用。", + "labels": null + }, + { + "number": 673, + "title": "opds support", + "html_url": "https://github.com/hectorqin/reader/issues/673", + "created_at": "2025-06-17T11:27:19Z", + "closed_at": "2026-09-23T09:17:32Z", + "state_reason": "not_planned", + "comments": 1, + "body": "大佬目前版本支持opds么", + "labels": null + }, + { + "number": 669, + "title": "求教服务器版本如何看漫画", + "html_url": "https://github.com/hectorqin/reader/issues/669", + "created_at": "2025-06-04T16:53:49Z", + "closed_at": "2026-09-23T09:17:31Z", + "state_reason": "not_planned", + "comments": 2, + "body": "提示不支持webview或者显示空白\n\n![Image](https://github.com/user-attachments/assets/a2ac2179-90c3-4493-8991-2f572dfc7170)\n\n", + "labels": null + }, + { + "number": 670, + "title": "[web/server] docker compose部署 out of memory", + "html_url": "https://github.com/hectorqin/reader/issues/670", + "created_at": "2025-06-05T04:00:30Z", + "closed_at": "2026-09-23T09:17:31Z", + "state_reason": "not_planned", + "comments": 1, + "body": "**为避免无效问题和冗余问题,提问前请确认**\n1. 你确定Google不能解决你的问题\n2. 你确定已有的issue不能解决你的问题\n3. 你确定issue的title按照格式如下:[web/simple-web/server]:description\n\n**Describe the bug 描述你遇到的问题**\n docker compose部署 docker启动时报错\n```\nlibrary initialization failed - unable to allocate file descriptor table - out of memory11:49:36.914 [main] INFO com.htmake.reader.ReaderApplication - Starting application with args: []\n```\n 机器内存16G\n```\ndnine7@dnine7-GL63-8RE:~/reader$ free -h\n total used free shared buff/cache available\n内存: 14Gi 4.9Gi 9.3Gi 265Mi 1.3Gi 10Gi\n交换: 4.0Gi 0B 4.0Gi\n```\n\n**版本说明**\n - OS: [e.g. win] 说明操作系统 ubuntu 24.10\n - Deploy Method 说明软件部署方式 docker compose\n - Program Version 说明软件版本 3.2.14 \n\n**docker-compose-yaml**\n```\nversion: '3.1'\nservices:\n# reader 在线阅读\n# 公开服务器(服务器位于日本):[https://reader.nxnow.top](https://reader.nxnow.top) 测试账号/密码分别为guest/guest123,也可自行创建账号添加书源,不定期删除长期未登录账号(2周)\n# 书源集合 : [https://legado.aoaostar.com/](https://legado.aoaostar.com/) 点击打开连接,添加远程书源即可\n# 公众号汇总 : [https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MzMyMDgyMA==&action=getalbum&album_id=2397535253763801090#wechat_redirect](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MzMyMDgyMA==&action=getalbum&album_id=2397535253763801090#wechat_redirect)\n# 手动更新方式 : docker-compose pull && docker-compose up -d\n reader:\n image: hectorqin/reader:latest\n #image: hectorqin/reader:openj9-latest #docker镜像,arm64架构或小内存机器优先使用此镜像.启用需删除上一行\n container_name: reader #容器名 可自行修改\n restart: always\n ports:\n - 9970:8080 #4396端口映射可自行修改\n networks:\n - share_net\n volumes:\n - /home/dnine7/reader/logs:/logs #log映射目录 /home/reader/logs 映射目录可自行修改\n - /home/dnine7/reader/storage:/storage #数据映射目录 /home/reader/storage 映射目录可自行修改\n environment:\n - SPRING_PROFILES_ACTIVE=prod\n - READER_APP_USERLIMIT=999999 #用户上限,默认50\n - READER_APP_USERBOOKLIMIT=99999 #用户书籍上限,默认200\n - READER_APP_CACHECHAPTERCONTENT=true #开启缓存章节内容 V2.0\n # 如果启用远程webview,需要取消注释下面的 remote-webview 服务\n # - READER_APP_REMOTEWEBVIEWAPI=http://remote-webview:8050 #开启远程webview\n # 下面都是多用户模式配置\n - READER_APP_SECURE=true #开启登录鉴权,开启后将支持多用户模式\n - READER_APP_SECUREKEY=xzq521xzq #管理员密码 建议修改\n - READER_APP_INVITECODE=Dnine7 #注册邀请码 建议修改,如不需要可注释或删除\n # remote-webview:\n # image: hectorqin/remote-webview\n # container_name: remote-webview #容器名 可自行修改\n # restart: always\n # ports:\n # - 8050:8050\n # networks:\n # - share_net\n# 自动更新docker镜像\n watchtower:\n image: containrrr/watchtower\n container_name: watchtower\n restart: always\n # 环境变量,设置为上海时区\n environment:\n - TZ=Asia/Shanghai\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n command: reader watchtower --cleanup --schedule \"0 0 4 * * *\"\n networks:\n - share_net\n # 仅更新reader与watchtower容器,如需其他自行添加 '容器名' ,如:reader watchtower nginx\n # --cleanup 更新后清理旧版本镜像\n # --schedule 自动检测更新 crontab定时(限定6位crontab) 此处代表凌晨4点整\nnetworks:\n share_net:\n driver: bridge\n```\n", + "labels": null + }, + { + "number": 665, + "title": "[web/server] claw cloud run 和Railway 部署都失败", + "html_url": "https://github.com/hectorqin/reader/issues/665", + "created_at": "2025-05-30T13:35:48Z", + "closed_at": "2026-09-23T09:17:30Z", + "state_reason": "not_planned", + "comments": 1, + "body": "![Image](https://github.com/user-attachments/assets/3df583a0-cdec-4d39-a5e6-187048f60d00)\n\n![Image](https://github.com/user-attachments/assets/3eb8f049-5c62-4c81-827e-f90c3c940ae7)\n\n![Image](https://github.com/user-attachments/assets/bac806fb-762c-4ffe-86be-2e0e97034ec8)\n\n部署方式:https://mp.weixin.qq.com/s/TCL6i_7Zrgf2okJG-BF07Q\n如果有成功的大佬请帮帮忙看一下,是怎么回事?要怎么处理,还是说当前的这个版本是不能部署的?能部署的是哪个版本,有没有对应的链接,\n\n感谢各位大佬!!!", + "labels": null + }, + { + "number": 666, + "title": "部署那个就可以吗", + "html_url": "https://github.com/hectorqin/reader/issues/666", + "created_at": "2025-05-30T14:02:54Z", + "closed_at": "2026-09-23T09:17:30Z", + "state_reason": "not_planned", + "comments": 0, + "body": "> Reader 完整源码仅开放到 v2.5.4,新版本当前仅开放部分开源源码,见 https://github.com/hectorqin/reader-legado.\n> \n> \n> \n> @jueinin \n> \n> 早就是闭源的了 \n\n _Originally posted by @rolay in [#498](https://github.com/hectorqin/reader/issues/498#issuecomment-2313894063)_\n\n那意思是部署上面的这个就行吗? https://github.com/hectorqin/reader-legado.", + "labels": null + }, + { + "number": 664, + "title": "电脑上3.2.14书架和目录的排列", + "html_url": "https://github.com/hectorqin/reader/issues/664", + "created_at": "2025-05-29T23:47:13Z", + "closed_at": "2026-09-23T09:17:30Z", + "state_reason": "not_planned", + "comments": 2, + "body": "电脑上3.2.14书架和目录的排列都是类似手机的单列竖排,没有哪里可以设置成跟以前一样的吗\n\n书架左边栏找到了书架设置,目录没找到。\n\n但是书架,如果设置成网格显示,就只显示封面,不显示已读和最新章节,且无论点击还是长按都是直接点进小说里,右键无效,也就是无法像列表模式点击封面那样出现编辑,移出书架,封面设置等选项。\n像保留这些选项就只能设置成单列的列表模式,希望可以保留跟以前的书架一样的多列列表的模式。另外就是目录的多列显示哪里设置", + "labels": null + }, + { + "number": 659, + "title": "使用自定义目录规则会导致无法调整左右间距(手机模式)", + "html_url": "https://github.com/hectorqin/reader/issues/659", + "created_at": "2025-05-14T17:59:39Z", + "closed_at": "2026-09-23T09:17:29Z", + "state_reason": "not_planned", + "comments": 0, + "body": "使用自定义目录规则会导致部分小说无法调整左右间距\n\n小说来源:本地txt\n\n版本:reader-pro-3.2.13.jar\n\n问题出现在web手机模式。\n\n使用的规则是自定义的冒号分割\n\n正则表达式:`.*(?<=:)\\s([\\u4e00-\\u9fa5a-zA-Z0-9]{1,7})[^\\n]{1,35}(|\\n)`\n\n\n> 以下是使用自定义的目录规则,无法设置左右间距\n\n![Image](https://github.com/user-attachments/assets/38d5f327-eaad-4105-aae7-52cf32cdcb94)\n\n> 以下是使用自带的目录规则,可以正常设置左右间距\n\n![Image](https://github.com/user-attachments/assets/ee94fbb3-382c-443d-a454-834225c84d55)", + "labels": null + }, + { + "number": 663, + "title": "[web/server]:阅读了几章后,点击顶部首页按钮返回书架页后,阅读进度没有刷新,还是显示之前阅读进度,点击进入后也会回到之前的阅读进度", + "html_url": "https://github.com/hectorqin/reader/issues/663", + "created_at": "2025-05-28T23:24:28Z", + "closed_at": "2026-09-23T09:17:29Z", + "state_reason": "not_planned", + "comments": 4, + "body": "**为避免无效问题和冗余问题,提问前请确认**\n1. 你确定Google不能解决你的问题\n2. 你确定已有的issue不能解决你的问题\n3. 你确定issue的title按照格式如下:[web/simple-web/server]:description\n\n**Describe the bug 描述你遇到的问题**\nA clear and concise description of what the bug is. 简洁有效的说明。\n\n阅读小说中点击首页按钮,书籍进度不会刷新\n\n**To Reproduce 如何重现问题**\nSteps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容:\n1. Go to 小说阅读界面,阅读几章之后\n2. Click on '首页'\n3. 可以看到书架上刚刚阅读的书,进度并没有更新,点进去也会回到之前的章节。除非点首页返回后,再次下拉刷新后,书架上的进度才会更新\n\n**Expected behavior 期待修复的效果**\nA clear and concise description of what you expected to happen. 简单描述。\n\n点首页返回后,能正确刷新进度\n\n**Screenshots 如有必要,可以截图说明**\nIf applicable, add screenshots to help explain your problem.\n\n**版本说明**\n - OS: [e.g. win] unraid\n - Deploy Method docker compose\n - Program Version v3.2.14-05250935\n - Browser [e.g. chrome, safari] win edge, android edge\n\n**Additional context 其他说明**\nAdd any other context about the problem here. 添加你认为有必要的内容,否则不写。\n", + "labels": null + }, + { + "number": 658, + "title": "提供 t2s 等新版开源阅读js命令", + "html_url": "https://github.com/hectorqin/reader/issues/658", + "created_at": "2025-05-13T11:07:33Z", + "closed_at": "2026-09-23T09:17:28Z", + "state_reason": "not_planned", + "comments": 0, + "body": "如题", + "labels": null + }, + { + "number": 657, + "title": "原先导入的小说,突然在页面中不显示了", + "html_url": "https://github.com/hectorqin/reader/issues/657", + "created_at": "2025-05-05T03:59:23Z", + "closed_at": "2026-09-23T09:17:28Z", + "state_reason": "not_planned", + "comments": 1, + "body": "Docker部署的,书源能正常显示,但是小说却不显示了。并且我去容器内看了,小说数据其实都还是在的,只是不知道为什么页面不显示了【难道小说的展示与否并不是根据存储目录中是否有这本小说,而是有一个专门的[配置文件]记录每次导入的新小说,然后页面展示小说就是根据这个[配置文件]来展示?然后莫名其妙这个[配置文件]消失了,所以原先导入的小说都不展示了?然后我重新导入了一本新的,于是这个[配置文件]重建了,所以才会只显示导入的这一本小说?】\n\n![Image](https://github.com/user-attachments/assets/bf81474e-2796-443d-b027-ce2e36ed2828)\n\n![Image](https://github.com/user-attachments/assets/ea4e4964-b177-491c-b515-e4c0573b6d8c)", + "labels": null + }, + { + "number": 656, + "title": "[server]macOS下的safari导出书籍时,文件名乱码", + "html_url": "https://github.com/hectorqin/reader/issues/656", + "created_at": "2025-04-18T15:13:33Z", + "closed_at": "2026-09-23T09:17:27Z", + "state_reason": "not_planned", + "comments": 0, + "body": "如题,文件名中的中文变成URL编码。\n\n**To Reproduce 如何重现问题**\n只要导出书籍必定会出现。\n\n**版本说明**\n - OS: [e.g. win] macOS15.4.1\n - Deploy Method 说明软件部署方式 群晖Docker\n \n**Additional context 其他说明**\nAI说是部分服务器未遵循标准,在返回文件时未正确设置 Content-Disposition 头部的 filename* 参数(用于 UTF-8 编码),导致 Safari 无法自动解码文件名。Safari 对非 ASCII 字符(如中文、日文、特殊符号)的处理较严格,若服务器未明确指定 UTF-8 编码,Safari 会直接保留 URL 编码格式,而非尝试解码。\n", + "labels": null + }, + { + "number": 654, + "title": "目录规则:添加自定义规则以及设置默认规则", + "html_url": "https://github.com/hectorqin/reader/issues/654", + "created_at": "2025-04-15T04:26:39Z", + "closed_at": "2026-09-23T09:17:27Z", + "state_reason": "not_planned", + "comments": 1, + "body": "关于目录规则的两个建议:\n1、添加自定义规则(目前只有内置的一些规则,添加完书籍后,然后在阅读界面可重选以及修改规则,但是下次再添加书籍时要用相同规则需要重新加一遍)\n2、设置某个规则为默认规则(目前是内置规则)\n![Image](https://github.com/user-attachments/assets/15385785-340f-4db1-8647-613582d5b346)", + "labels": null + }, + { + "number": 655, + "title": "服务器版本老是跳,有时候跳段落,有时候跳章节", + "html_url": "https://github.com/hectorqin/reader/issues/655", + "created_at": "2025-04-15T15:48:40Z", + "closed_at": "2026-09-23T09:17:27Z", + "state_reason": "not_planned", + "comments": 3, + "body": "![Image](https://github.com/user-attachments/assets/1d72e022-360f-41f3-bad0-09c24a9ac282)\n\n这样设置有问题吗", + "labels": null + }, + { + "number": 651, + "title": "调试源结果正常,但是在阅读UI界面添加书籍,反应找不到书,怎么回事?", + "html_url": "https://github.com/hectorqin/reader/issues/651", + "created_at": "2025-04-12T04:33:06Z", + "closed_at": "2026-09-23T09:17:26Z", + "state_reason": "not_planned", + "comments": 0, + "body": "阅读docker版本 v3.2.12-12171550。系统ubuntu 24.04.2。浏览器firefox,版本137.0。\n写了一个书源,书源内容见以下。\n```\n{\n \"bookSourceUrl\": \"https://www.xiguasuwu.com/\",\n \"bookSourceType\": 0,\n \"bookSourceName\": \"西瓜书屋\",\n \"bookSourceGroup\": \"自编\",\n \"bookSourceComment\": \"\",\n \"loginUrl\": \"\",\n \"loginUi\": \"\",\n \"loginCheckJs\": \"\",\n \"concurrentRate\": \"\",\n \"header\": \" {\\n'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36 Edg/90.0.818.56',\\n'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',\\n'Accept-Encoding': 'gzip, deflate, br',\\n'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6'\\n}\",\n \"bookUrlPattern\": \"\",\n \"searchUrl\": \"https://www.xiguasuwu.com/search,{'method':'POST','body':'searchkey={{key}}'}\",\n \"exploreUrl\": \"\",\n \"enabled\": true,\n \"enabledExplore\": true,\n \"weight\": 0,\n \"customOrder\": 0,\n \"lastUpdateTime\": 1744422386179,\n \"ruleSearch\": {\n \"bookList\": \"class.panel-body@class.book-coverlist\",\n \"name\": \"class.fs-16@tag.a@text\",\n \"author\": \"class.fs-14@text\",\n \"intro\": \"class.fs-12@text\",\n \"bookUrl\": \"class.fs-16@tag.a@href\"\n },\n \"ruleExplore\": {},\n \"ruleBookInfo\": {\n \"name\": \"class.bookTitle@text\",\n \"author\": \"class.glyphicon-user@text\",\n \"kind\": \"class.breadcrumb@tag.li1@text\",\n \"wordCount\": \"class.glyphicon-font@text\",\n \"lastChapter\": \"class.text-danger@text\",\n \"intro\": \"id.bookIntro@text\",\n \"coverUrl\": \"class.img-thumbnail@data-original\",\n \"tocUrl\": \"class.btn-success@href\"\n },\n \"ruleToc\": {\n \"chapterList\": \"id.newlist@tag.dd\",\n \"chapterName\": \"class.col-sm-4@tag.a@text\",\n \"chapterUrl\": \"class.col-sm-4@tag.a@href\",\n \"nextTocUrl\": \"class.right@tag.a@href!class.before\"\n },\n \"ruleContent\": {\n \"content\": \"id.booktxt@text\",\n \"nextContentUrl\": \"id.linkNext@href\"\n }\n}\n```\n\n调试源,可以得到正文页解析完成。调试回报见以下附件。\n[阅读调试log.txt](https://github.com/user-attachments/files/19717654/log.txt)\n然后将书源保存,确认阅读UI界面里有该书源。然后只用该源分类,搜索书籍,反馈找不到书。书名为“绿龙筑巢记”,即是调试源时用的搜索书名。不知道是什么原因,有朋友知道什么原因,或者怎么检查?\n[reader-2025-04-12.log](https://github.com/user-attachments/files/19716378/reader-2025-04-12.log)\n阅读的日志文件,隐去个人网址。", + "labels": null + }, + { + "number": 652, + "title": "移动端浏览器进度异常跳跃问题【尝试解决中】", + "html_url": "https://github.com/hectorqin/reader/issues/652", + "created_at": "2025-04-12T05:29:04Z", + "closed_at": "2026-09-23T09:17:26Z", + "state_reason": "not_planned", + "comments": 16, + "body": "1、屏幕触控、滑动经常乱跳乱翻页\n2、浏览器界面不能全屏,建议去komga漫画应用学习一下", + "labels": null + }, + { + "number": 650, + "title": "请求支持startBrowserAwait去除人机验证", + "html_url": "https://github.com/hectorqin/reader/issues/650", + "created_at": "2025-04-10T12:03:07Z", + "closed_at": "2026-09-23T09:17:25Z", + "state_reason": "not_planned", + "comments": 1, + "body": "现在大部分的网站都启用了人机验证,大部分的书源通过以下方案去除人机验证:\n\nif(result.match(/Just a moment/)){\ncookie.removeCookie(source.bookSourceUrl)\nvar x=baseUrl;\nvar s=java.startBrowserAwait(x,\"验证\").body()\nresult=s;\n\t}else{\n\t\tresult=result;\n\t\t}\nresult\n\n\nreader3服务器版目前还不支持,使用时时会报错\nError: org.mozilla.javascript.EcmaError: TypeError: Cannot find function startBrowserAwait in object io.legado.app.model.analyzeRule.AnalyzeRule@1baa25b8\n请问大佬是否可以支持,谢谢", + "labels": null + }, + { + "number": 649, + "title": "求助:目录能否自动刷新", + "html_url": "https://github.com/hectorqin/reader/issues/649", + "created_at": "2025-04-10T07:44:39Z", + "closed_at": "2026-09-23T09:17:25Z", + "state_reason": "not_planned", + "comments": 1, + "body": "docker部署的阅读服务端。\n使用本地txt作为书籍来源,作为来源的本地txt内容会自动更新。\n但本地更新后在阅读web界面书本不会自动更新内容,需要在书本详情里点击目录-刷新,才能更新章节及内容。\n想请问大佬有没有解决方法或者是能够调用的章节刷新接口,谢谢!", + "labels": null + }, + { + "number": 647, + "title": "大佬求助:字符集错误导致文本无法正常显示", + "html_url": "https://github.com/hectorqin/reader/issues/647", + "created_at": "2025-04-09T05:17:35Z", + "closed_at": "2026-09-23T09:16:52Z", + "state_reason": "not_planned", + "comments": 1, + "body": "**为避免无效问题和冗余问题,提问前请确认**\n1. 你确定Google不能解决你的问题\n2. 你确定已有的issue不能解决你的问题\n3. 你确定issue的title按照格式如下:[web/simple-web/server]:description\n\n**Describe the bug 描述你遇到的问题**\n书源可以缓存,但由于字符集不匹配无法正常显示\n可以正常缓存,获取封面,获取目录\n在reader安卓原版客户端上使用正常\n\n**To Reproduce 如何重现问题**\nSteps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容:\n1. 添加“速读谷”书源 https://www.yckceo.com/yuedu/shuyuan/json/id/5862.json\n2. 随便添加一本书到书架,我添加的是“夜无疆”\n3. 打开书籍\n4. 随便选中间的一个章节加载(这里很奇怪,第一次用这个书源的时候前两个章节都可以正常加载,后面突然不行了)\n5. 页面显示java字符集报错\n\n![Image](https://github.com/user-attachments/assets/efa13e74-c9cb-44e4-8099-fc8cb3f540d5)\n\n对应log\n```\n[f26ccad3] [DefaultDispatcher-worker-2] 12:50:59.249 ERROR YueduApi - onHandlerError: \njava.nio.charset.IllegalCharsetNameException: initial-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width\n\tat java.nio.charset.Charset.checkName(Charset.java:315)\n\tat java.nio.charset.Charset.lookup2(Charset.java:484)\n\tat java.nio.charset.Charset.lookup(Charset.java:464)\n\tat java.nio.charset.Charset.forName(Charset.java:528)\n\tat io.legado.app.help.http.OkHttpUtilsKt.text(OkHttpUtils.kt:117)\n\tat io.legado.app.help.http.OkHttpUtilsKt.text$default(OkHttpUtils.kt:102)\n\tat io.legado.app.help.http.OkHttpUtilsKt.newCallStrResponse(OkHttpUtils.kt:78)\n\tat io.legado.app.help.http.OkHttpUtilsKt$newCallStrResponse$1.invokeSuspend(OkHttpUtils.kt)\n\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)\n\tat kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)\n\tat kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)\n\tat kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)\n\tat kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)\n\tat kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)\n```\n\n**Expected behavior 期待修复的效果**\n可以正常显示章节文本\n\n**Screenshots 如有必要,可以截图说明**\n\n\n**版本说明**\n - OS: [e.g. win] Linux\n - Deploy Method Docker\n - Program Version 3.2.13\n - Browser [e.g. chrome, safari] Chrome\n\n**Additional context 其他说明**\nAdd any other context about the problem here. 添加你认为有必要的内容,否则不写。\n", + "labels": null + }, + { + "number": 645, + "title": "[server]翻页模式bug", + "html_url": "https://github.com/hectorqin/reader/issues/645", + "created_at": "2025-04-07T16:48:19Z", + "closed_at": "2026-09-23T09:16:52Z", + "state_reason": "not_planned", + "comments": 3, + "body": "“上下滑动”翻页模式在章末会有大片的空白。\n\n版本说明\n\n - OS: [iOS]\n - Deploy Method docker\n - Program Version v3.2.13\n - Browser [safari]", + "labels": "enhancement" + }, + { + "number": 644, + "title": "docker版目录", + "html_url": "https://github.com/hectorqin/reader/issues/644", + "created_at": "2025-04-07T05:21:03Z", + "closed_at": "2026-09-23T09:16:51Z", + "state_reason": "not_planned", + "comments": 4, + "body": "大佬你好,请问能否重置或指定nas里的目录,而不是root里面,就是扫描nas本地目录,从而添加书籍", + "labels": null + }, + { + "number": 642, + "title": "每次升级版本就会把之前上传的小说重置", + "html_url": "https://github.com/hectorqin/reader/issues/642", + "created_at": "2025-04-06T01:39:55Z", + "closed_at": "2026-09-23T09:16:51Z", + "state_reason": "not_planned", + "comments": 1, + "body": "我用docker安装的,每次升级版本就会把之前上传的小说重置\n\n![Image](https://github.com/user-attachments/assets/91522c5e-2f84-4542-8fd5-a24374d109fd)", + "labels": null + }, + { + "number": 643, + "title": "过滤规则:是否能够将文本替换为换行", + "html_url": "https://github.com/hectorqin/reader/issues/643", + "created_at": "2025-04-06T05:27:27Z", + "closed_at": "2026-09-23T09:16:51Z", + "state_reason": "not_planned", + "comments": 0, + "body": "我要将两个空格替换为换行,我在过滤规则中使用了/n , /r, /r/n,启动或是关闭“使用正则表达式”,都不成功,都是直接将两个空格替换为了/n , /r, /r/n的文本,请问过滤规则能够实现将文本替换为换行吗", + "labels": null + }, + { + "number": 638, + "title": "web/server 书架过大的时候会出现超时", + "html_url": "https://github.com/hectorqin/reader/issues/638", + "created_at": "2025-03-20T12:30:45Z", + "closed_at": "2026-09-23T09:16:50Z", + "state_reason": "not_planned", + "comments": 1, + "body": "**Describe the bug 描述你遇到的问题**\n无法加载书架数据,导致无法查看书籍\n\n2025-03-20 20:14:11 [09201f70] [DefaultDispatcher-worker-4] INFO c.h.r.api.controller.BaseController - Loop: 3681 concurrentCount: 16 lastIndex: 58910 endIndex: 58917 costTime: 37853 ms deferredList size: 0\n2025-03-20 20:14:11 [09201f70] [DefaultDispatcher-worker-4] INFO c.h.r.api.controller.BaseController - Loop: 3682 concurrentCount: 6 lastIndex: 58916 endIndex: 58917 costTime: 37863 ms deferredList size: 0\n2025-03-20 20:14:12 [09201f70] [DefaultDispatcher-worker-4] INFO i.v.e.w.h.impl.LoggerHandlerImpl - 10.0.3.1 - - [Thu, 20 Mar 2025 12:13:32 GMT] \"GET /reader3/getBookshelf?refresh=0&v=1742472812088 HTTP/1.1\" 200 40385249 \"https://公网IP/\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36\"\n2025-03-20 20:14:12 [1e01fe14] [vert.x-eventloop-thread-0] INFO c.h.reader.verticle.RestVerticle - 客户端已断开链接,终止运行\n\n![Image](https://github.com/user-attachments/assets/ceb65b7c-21d0-48a0-81e8-36327dff856b)\n超时后,后端设定会报红\n\n![Image](https://github.com/user-attachments/assets/e1df17c3-76bb-4427-99a9-5d729379c8c8)\n我的书架文件bookshelf大概有60M\n\n**To Reproduce 如何重现问题**\nSteps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容:\n进入页面开始加载书架就会出现,完全无法加载书籍\n\n**Expected behavior 期待修复的效果**\n希望书架信息可以在项目启动时预加载到缓存中,更新数据的时候,对缓存也进行更新删除操作\n\n**版本说明**\n - OS: [e.g. win] 说明操作系统\n - linux\n - Deploy Method 说明软件部署方式\n - docker\n - Program Version 说明软件版本\n - 3.2.12\n - Browser [e.g. chrome, safari] 说明终端、浏览器型号\n - chrome\n\n**Additional context 其他说明**\nAdd any other context about the problem here. 添加你认为有必要的内容,否则不写。\n", + "labels": null + }, + { + "number": 641, + "title": "[server] 关于自动同步配置造成的问题", + "html_url": "https://github.com/hectorqin/reader/issues/641", + "created_at": "2025-04-05T16:16:26Z", + "closed_at": "2026-09-23T09:16:50Z", + "state_reason": "not_planned", + "comments": 0, + "body": "在同时打开“自动同步”和配置方案“自动切换”时,自动切换日/夜模式会失效。\n\n表现为在冷启动时,先获取了系统的深色模式,再触发了自动同步,同步后把在系统获取到的深色模式覆盖掉了。\n\n\n**版本说明**\n - OS: [iOS]\n - Deploy Method docker\n - Program Version v3.2.13\n - Browser [safari]", + "labels": null + }, + { + "number": 637, + "title": "每过一段时间,都会自动删除我使用的账号", + "html_url": "https://github.com/hectorqin/reader/issues/637", + "created_at": "2025-03-15T09:08:22Z", + "closed_at": "2026-09-23T09:16:49Z", + "state_reason": "not_planned", + "comments": 6, + "body": "**Describe the bug 描述你遇到的问题**\n1.每过一段时间没登录,都会自动删除我使用的账号,登录提示账号未注册\n2.重新注册同名账号后,所有数据都在,仅仅是账号莫名其妙消失了\n3.大概每过1个月左右?如果没登录的话,账号就不见了,并且我并未设置多少天未登录自动删除账号这个环境变量\n\n**To Reproduce 如何重现问题**\n1.重现过两次,每次都是长时间未登录,然后账号消失,但账号的数据却又都在\n\n**Expected behavior 期待修复的效果**\n1.希望能找到账号被删除的原因,毕竟我并未设置“**天未登录就自动删除”的环境变量,还是说docker在未设置环境变量的情况下,本身就默认自带“30天未登录就删除账号”这个环境变量?我用了几年了,记得以前不会的,不知道从去年哪个版本开始就开始会删账号了\n\n**版本说明**\nUNRAID系统下,docker部署\n\n\n", + "labels": null + }, + { + "number": 636, + "title": "阅读epub时,iframe和解析模式的bug", + "html_url": "https://github.com/hectorqin/reader/issues/636", + "created_at": "2025-03-12T04:43:30Z", + "closed_at": "2026-09-23T09:16:49Z", + "state_reason": "not_planned", + "comments": 0, + "body": "\n**Describe the bug 描述你遇到的问题**\n阅读epub时,iframe和解析模式的bug\n\n**To Reproduce 如何重现问题**\n1. 将epub解析设置成iframe模式\n2. 显示为这样(标题和图片重叠,字体样式有问题)\n![Image](https://github.com/user-attachments/assets/f684880c-f806-4b9a-9c65-d622a1224447)\n3.shitf+f5忽略缓存刷新后,将epub解析设置为解析模式\n4.显示正常\n![Image](https://github.com/user-attachments/assets/9b06e98c-fccb-4c12-ad88-5d037d8c963c)\n5.点击下一章或者刷新页面后,格式错乱(之后无论怎么刷新或者切换解析模式都是错乱的)\n![Image](https://github.com/user-attachments/assets/d7db2f09-558a-4244-938c-7cfb00b95726)\n6.将epub解析再次设置为iframe模式,shitf+f5忽略缓存刷新,之后再将epub解析设置为解析模式。显示正常\n但是点击下一章或者刷新页面后还是格式错乱\n结论:只有将epub解析设置成iframe模式后shitf+f5忽略缓存刷新,再设置成解析模式,才能出现一次正常显示的页面,之后无论点击下一章还是刷新页面都是错乱的,需要重复此步骤才能再次正常显示\n\n\n**Expected behavior 期待修复的效果**\n设置成解析模式时无论点击下一章还是刷新页面都正常显示\n\n**版本说明**\n - OS: LINUX\n - Deploy Method: docker\n - Program Version : v3.2.12\n - Browser : chrome浏览器(https访问)\n\n**Additional context 其他说明**\nepub文件(下载后解压):\n[epub文件.zip](https://github.com/user-attachments/files/19202858/epub.zip)", + "labels": null + }, + { + "number": 635, + "title": "在Arc浏览器使用reader的朗读功能,跳到下一章的时候,总是从文章的中间开始。", + "html_url": "https://github.com/hectorqin/reader/issues/635", + "created_at": "2025-03-10T02:07:11Z", + "closed_at": "2026-09-23T09:16:48Z", + "state_reason": "not_planned", + "comments": 3, + "body": "在Arc浏览器使用reader的朗读功能,跳到下一章的时候,总是从文章的中间开始,~~但是使用chrome并没有这个问题~~(听了一上午的chrome,也发现存在这个问题),可以随意复现。\n\n**版本说明**\n - macos Sonoma 14.0\n - docker本地部署\n - 软件版本:V3.2.12-12171550\n - arc版本:版本1.85.0 (59596)\n - 内核:Chromium Engine Version 134.0.6998.45\n", + "labels": null + }, + { + "number": 632, + "title": "当使用mongo数据库时,依然会丢失书源", + "html_url": "https://github.com/hectorqin/reader/issues/632", + "created_at": "2025-03-05T07:05:09Z", + "closed_at": "2026-09-23T09:16:48Z", + "state_reason": "not_planned", + "comments": 1, + "body": "**为避免无效问题和冗余问题,提问前请确认**\n1. 你确定Google不能解决你的问题\n2. 你确定已有的issue不能解决你的问题\n3. 你确定issue的title按照格式如下:[web/simple-web/server]:description\n\n**Describe the bug 描述你遇到的问题**\nA clear and concise description of what the bug is. 简洁有效的说明。\n\n**To Reproduce 如何重现问题**\nSteps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior 期待修复的效果**\nA clear and concise description of what you expected to happen. 简单描述。\n\n**Screenshots 如有必要,可以截图说明**\nIf applicable, add screenshots to help explain your problem.\n\n**版本说明**\n - OS: [e.g. win] 说明操作系统\n - Deploy Method 说明软件部署方式\n - Program Version 说明软件版本\n - Browser [e.g. chrome, safari] 说明终端、浏览器型号\n\n**Additional context 其他说明**\nAdd any other context about the problem here. 添加你认为有必要的内容,否则不写。\n", + "labels": null + }, + { + "number": 634, + "title": "[web/simple]:kindle 的simple-waeb翻页闪屏", + "html_url": "https://github.com/hectorqin/reader/issues/634", + "created_at": "2025-03-07T16:21:34Z", + "closed_at": "2026-09-23T09:16:48Z", + "state_reason": "not_planned", + "comments": 1, + "body": "kindle在使用simple-web版的网页翻页时会出现闪屏(全屏刷新)问题,但使用正常版网页,并不会出现闪屏。在正常版的网页使用时,将动画时长设定为0,动画速度设定为1000,无闪屏,且使用体验还可以。但是正常版的内存占用太大,使用卡顿。能否增加一个类似的翻页效果,来解决全屏刷新的问题。\n\n\n**版本说明**\n - OS: kindle\n - Deploy Method kindle 浏览器\n - Program Version 3.2.12\n - Browser kindle withe paper 5 ,内置浏览器\n\n**Additional context 其他说明**\nkpw 5有水波纹刷新,也许增加一个类似的动画。", + "labels": null + }, + { + "number": 630, + "title": "从书仓导入PDF文件,大部分页面变成空白", + "html_url": "https://github.com/hectorqin/reader/issues/630", + "created_at": "2025-03-01T03:32:22Z", + "closed_at": "2026-09-23T09:16:47Z", + "state_reason": "not_planned", + "comments": 0, + "body": "**为避免无效问题和冗余问题,提问前请确认**\n1. 你确定Google不能解决你的问题\n2. 你确定已有的issue不能解决你的问题\n3. 你确定issue的title按照格式如下:[web/simple-web/server]:description\n\n**Describe the bug 描述你遇到的问题**\n从书仓导入PDF文件,大部分页面变成空白。\n\n**To Reproduce 如何重现问题**\nSteps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior 期待修复的效果**\nA clear and concise description of what you expected to happen. 简单描述。\n\n**Screenshots 如有必要,可以截图说明**\nIf applicable, add screenshots to help explain your problem.\n\n**版本说明**\n - OS: [e.g. win] MacOS 15.3.1\n - Deploy Method 群晖docker\n - Program Version v3.2.12-12171550\n - Browser [e.g. chrome, safari] safari\n\n\"Image\"\n\n\"Image\"\n\n\"Image\"\n\n**Additional context 其他说明**\nAdd any other context about the problem here. 添加你认为有必要的内容,否则不写。\n", + "labels": null + }, + { + "number": 626, + "title": "希望把java端整合在setup", + "html_url": "https://github.com/hectorqin/reader/issues/626", + "created_at": "2025-02-08T08:42:02Z", + "closed_at": "2026-09-23T09:16:47Z", + "state_reason": "not_planned", + "comments": 0, + "body": "希望把java端整合在setup,不是每个人都需要java,只是为了方便使用reader,而单独去安装一个java,没必要。这里指win端 ,web端还好,可以用apt,yum等直接安装,而且作者给的123盘的java链接已经失效", + "labels": null + }, + { + "number": 624, + "title": "翻页。。。在平板上不行啊,上下页不衔接", + "html_url": "https://github.com/hectorqin/reader/issues/624", + "created_at": "2025-01-28T02:31:18Z", + "closed_at": "2026-09-23T09:16:46Z", + "state_reason": "not_planned", + "comments": 2, + "body": "翻页。。。在平板上不行啊,上下页不衔接", + "labels": null + }, + { + "number": 622, + "title": "可以隐藏任务栏吗", + "html_url": "https://github.com/hectorqin/reader/issues/622", + "created_at": "2025-01-13T01:29:58Z", + "closed_at": "2026-09-23T09:16:46Z", + "state_reason": "not_planned", + "comments": 0, + "body": "**为避免无效问题和冗余问题,提问前请确认**\r\n1. 你确定Google不能解决你的问题\r\n2. 你确定已有的issue不能解决你的问题\r\n3. 你确定issue的title按照格式如下:[web/simple-web/server]:description\r\n\r\n**Describe the bug 描述你遇到的问题**\r\nA clear and concise description of what the bug is. 简洁有效的说明。\r\n\r\n**To Reproduce 如何重现问题**\r\nSteps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容:\r\n1. Go to '...'\r\n2. Click on '....'\r\n3. Scroll down to '....'\r\n4. See error\r\n\r\n**Expected behavior 期待修复的效果**\r\nA clear and concise description of what you expected to happen. 简单描述。\r\n\r\n**Screenshots 如有必要,可以截图说明**\r\nIf applicable, add screenshots to help explain your problem.\r\n\r\n**版本说明**\r\n - OS: [e.g. win] 说明操作系统\r\n - Deploy Method 说明软件部署方式\r\n - Program Version 说明软件版本\r\n - Browser [e.g. chrome, safari] 说明终端、浏览器型号\r\n\r\n**Additional context 其他说明**\r\nAdd any other context about the problem here. 添加你认为有必要的内容,否则不写。\r\n", + "labels": null + }, + { + "number": 623, + "title": "[web]:IOS设备使用safari等webkit内核浏览器在最新版本系统中无法使用大声朗读\\httpTTS的tts", + "html_url": "https://github.com/hectorqin/reader/issues/623", + "created_at": "2025-01-18T15:57:29Z", + "closed_at": "2026-09-23T09:16:46Z", + "state_reason": "not_planned", + "comments": 4, + "body": "**Describe the bug 描述你遇到的问题**\nIOS设备使用safari等webkit内核浏览器在最新版本系统中无法使用大声朗读\\HTTPTTS的tts\n\n**To Reproduce 如何重现问题**\nSteps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容:\n1. 打开任意文字内容\n2. 打开TTS版面\n3. 朗读方式选取大声朗读或者httpTTS(已经配置好AZURE TTS)\n4. 点击开始按钮,按钮没有变成暂停键,同时文字TTS部分被标红,就没有任何反应了\n\n**Screenshots 如有必要,可以截图说明**\n\n![Image](https://github.com/user-attachments/assets/7ae699e4-4387-4804-bd85-13bbd39f9391)\n\n![Image](https://github.com/user-attachments/assets/b33fbf53-fb13-4927-82b8-e3920335d696)\n\n**版本说明**\n - OS:iPhone/iPad iOS18.2\n - Deploy Method :Docker In DSM7.2.2\n - Program Version :3.2.12\n - Browser :iOS浏览器:safari,firefox,chrome,edge\n\n**Additional context 其他说明**\n经过穷举法,目前测试出只有夸克可以正常播放(见下图),理论上应该也是Webkit内核才对,原因不明\n其他所有设备都能正常播放。\n\n![Image](https://github.com/user-attachments/assets/8e3a8172-d011-4dd3-acef-1d7c0e479d18)", + "labels": null + }, + { + "number": 618, + "title": "删除书籍后,assets目录下的export与cover未删除", + "html_url": "https://github.com/hectorqin/reader/issues/618", + "created_at": "2024-12-17T00:30:16Z", + "closed_at": "2026-09-23T09:16:45Z", + "state_reason": "not_planned", + "comments": 0, + "body": "点击导入书籍,上传txt格式的书,设置了封面,然后导出epub,之后删除这本书,发现`assets/用户/export `里面曾经导出过的epub文件还残留, `assets/用户/cover`里面曾经上传的封面图片还在,书多了以后,这些残留资源该如何清理?", + "labels": null + }, + { + "number": 616, + "title": "大声朗读局域网IP可以使用,使用花生壳域名访问无法使用 DMZ主机是开着的", + "html_url": "https://github.com/hectorqin/reader/issues/616", + "created_at": "2024-12-16T07:19:44Z", + "closed_at": "2026-09-23T09:16:45Z", + "state_reason": "not_planned", + "comments": 8, + "body": "能不能让大声朗读不要用梯子也可以使用", + "labels": null + }, + { + "number": 613, + "title": "分享一个自定义Docker TTS 语音项目 可配 web TTS 朗读", + "html_url": "https://github.com/hectorqin/reader/issues/613", + "created_at": "2024-12-07T15:44:54Z", + "closed_at": "2026-09-23T09:16:44Z", + "state_reason": "not_planned", + "comments": 15, + "body": "项目 https://github.com/zuoban/tts?tab=readme-ov-file\r\n\r\n使用方法\r\n1、自行配置上述Docker\r\n2、打开本项目reader的web页面,随便点开一部小说,然后点朗读图标\r\n3、点击HttpTTS添加,弹出的默认示例中名字可以自己定义,其他不需要输入,url输入这个\r\n`http://192.168.1.X:8080/tts?t={{java.encodeURI(speakText)}}&v=zh-CN-YunxiNeural&r={{java.encodeURI(speakSpeed)}}*4&p=0&o=audio-24khz-48kbitrate-mono-mp3`\r\n\r\n```\r\n参数列表:\r\n1. t: 文本内容 (必填)\r\n2. v: 语音名称 (可选), 默认为 zh-CN-XiaoxiaoMultilingualNeural\r\n3. r: 语速 (可选), 默认为 0\r\n4. p: 语调 (可选), 默认为 0\r\n5. o: 输出格式 (可选), 默认为audio-24khz-48kbitrate-mono-mp3\r\n```\r\n", + "labels": null + }, + { + "number": 614, + "title": "数据库+redis方案", + "html_url": "https://github.com/hectorqin/reader/issues/614", + "created_at": "2024-12-11T02:44:22Z", + "closed_at": "2026-09-23T09:16:44Z", + "state_reason": "not_planned", + "comments": 1, + "body": "**Describe the bug 描述你遇到的问题**\r\n自部署版本在导入书源、校验书源等步骤时运行卡顿,且消耗内存过大\r\n\r\n**To Reproduce 如何重现问题**\r\nSteps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容:\r\n1. 导入书源\r\n2. 选择本地书源文件导入(json格式)\r\n\r\n或\r\n\r\n1. 失效书源\r\n2. 检测书源\r\n\r\n**Expected behavior 期待修复的效果**\r\n1. 在检测书源是否失效页面增加 `停止` 、`取消` 按钮,点击取消能取消全部操作,点击停止能停止校验(已校验过的结果保留)\r\n![image](https://github.com/user-attachments/assets/9c3e0c70-2ca9-4e51-b042-184aa1601ce8)\r\n\r\n2. 新增连接外置数据库方法及说明,如MySQL、Postgres、Mongo、Sqllite等\r\n\r\n3. 新增连接外置redis加快缓存速度\r\n\r\n**版本说明**\r\n - OS: 群晖Synology\r\n - Deploy Method :Docker\r\n - Program Version :3.2.11\r\n - Browser :Chrome\r\n\r\n**Additional context 其他说明**\r\n1. 建议隐藏 `后端设定` 选项,改错了前端容易崩溃\r\n![image](https://github.com/user-attachments/assets/aa2afbb4-d0da-43a6-a6e4-f229ed749047)\r\n2. `WdbDav` 增加 `坚果云` 等第三方远程连接方式,以便和手机端通过第三方同步\r\n3. `其它` 改为 `关于` 并排版到最下面(与Github链接合并)\r\n![image](https://github.com/user-attachments/assets/5697a718-8ba0-4239-941b-7980caf21c6d)\r\n", + "labels": null + }, + { + "number": 608, + "title": "服务器版手机 edge 浏览器自带大声朗读无法播放", + "html_url": "https://github.com/hectorqin/reader/issues/608", + "created_at": "2024-11-11T13:21:53Z", + "closed_at": "2026-09-23T09:16:43Z", + "state_reason": "not_planned", + "comments": 1, + "body": "服务器版手机端用 edge 浏览器自带大声朗读无法播放,不是项目自带的阅读功能,电脑上面没问题![Screenshot_2024-11-11-21-13-32-293_com.microsoft.emmx.jpg](https://github.com/user-attachments/assets/647576fc-05b2-408a-8ac2-ef560e3857dc)\n\n用 legado 的 app web 服务来链接到 reader3 上就没问题![IMG_20241111_213707.jpg](https://github.com/user-attachments/assets/072a5414-fd4e-453e-9a84-50a081404130)\n\n![Screenshot_2024-11-11-21-34-24-932_com.microsoft.emmx.jpg](https://github.com/user-attachments/assets/52d881e5-fdfc-48bb-8374-9c35df9914a6)\n\n\n还有目录加载速度很慢", + "labels": null + }, + { + "number": 601, + "title": "添加本地书籍报错 获取章节内容失败! 章节文件不存在", + "html_url": "https://github.com/hectorqin/reader/issues/601", + "created_at": "2024-10-30T20:45:25Z", + "closed_at": "2026-09-23T09:16:43Z", + "state_reason": "not_planned", + "comments": 3, + "body": "\r\n![image](https://github.com/user-attachments/assets/f9aa7db0-553d-4c83-9c26-c6fde4eba7fe)\r\n", + "labels": null + }, + { + "number": 596, + "title": "页面显示的章节与打开书籍后显示的章节不一致", + "html_url": "https://github.com/hectorqin/reader/issues/596", + "created_at": "2024-10-22T11:50:07Z", + "closed_at": "2026-09-23T09:16:43Z", + "state_reason": "not_planned", + "comments": 0, + "body": "1.问题描述\r\n页面显示的章节与打开书籍后显示的章节不一致\r\n\r\n2.问题发生的位置\r\n安卓APP和本项目网页端\r\n\r\n3.问题的详细描述\r\n手机端阅读APP设置webdav同步,安卓APP和网页端均能正常同步章节。但是在点击进入书籍后,打开的章节与显示的不一致。\r\n\r\n4.问题复现\r\n手机端阅读APP阅读至543章,点开网页发现能正常同步进度\r\n\"1\"\r\n\r\n但是点开书籍后显示的章节却是536章\r\n\"2\"\r\n注:手机端安卓APP和网页端均存在该问题。\r\n\r\n\r\n\r\n\r\n**版本说明**\r\n安卓阅读APP 3.23.031210\r\n本项目的版本 v3.2.10-07270648\r\n浏览器 火狐、Safari \r\n部署方式docker\r\n\r\n\r\n", + "labels": "need-confirm" + }, + { + "number": 594, + "title": "【建议】希望编辑后可以永久生效,而不是刷新后就会失效", + "html_url": "https://github.com/hectorqin/reader/issues/594", + "created_at": "2024-10-13T04:20:05Z", + "closed_at": "2026-09-23T09:16:42Z", + "state_reason": "not_planned", + "comments": 0, + "body": "![1728792866759](https://github.com/user-attachments/assets/ed88e1dc-cef0-4b0f-99e3-47d95f02fac6)\r\n![1728792885992](https://github.com/user-attachments/assets/3c7c4e71-8687-49a3-bc17-df3920183667)\r\n因为小说内容可能会有错别字或者乱码之类的,我看到了就想手动改掉或者删除,但是现在这个编辑功能就有点鸡肋,没什么实际效果【感觉只是改了HTML,变更的内容并没有真的保存起来】,刷新页面又出来了", + "labels": null + }, + { + "number": 590, + "title": "请教下各位书友httptts 怎么配置", + "html_url": "https://github.com/hectorqin/reader/issues/590", + "created_at": "2024-09-24T06:30:13Z", + "closed_at": "2026-09-23T09:15:23Z", + "state_reason": "not_planned", + "comments": 1, + "body": null, + "labels": null + }, + { + "number": 585, + "title": "是否能修改下翻页的一个问题,章尾最后一页的排版,最后一行字始终是在页面最底部", + "html_url": "https://github.com/hectorqin/reader/issues/585", + "created_at": "2024-08-30T13:01:21Z", + "closed_at": "2026-09-23T09:15:22Z", + "state_reason": "not_planned", + "comments": 0, + "body": "当阅读到章尾时,最后一页的最后一行字始终是在页面最底部,而不是依据上一页最后内容来排版\r\n这样每次到章尾时,眼睛都要上下扫扫,才能找刚才看到哪了\r\n\r\n图一为 上一页最后内容\r\n![1](https://github.com/user-attachments/assets/7fd97a48-ee0b-42b9-ab42-55aa21d7927a)\r\n\r\n图二为 最后一页排版\r\n![2](https://github.com/user-attachments/assets/be94b45a-3862-4c20-a923-e288599d46eb)\r\n\r\n图三 希望能把上一页已阅读的内容隐藏了\r\n![3](https://github.com/user-attachments/assets/05f27315-f1ef-4c91-99a3-e3f58e11f1ed)\r\n", + "labels": null + }, + { + "number": 588, + "title": "windows桌面版和安卓app版怎么和服务端连接", + "html_url": "https://github.com/hectorqin/reader/issues/588", + "created_at": "2024-09-08T16:49:23Z", + "closed_at": "2026-09-23T09:15:22Z", + "state_reason": "not_planned", + "comments": 6, + "body": "windows桌面版和安卓app版怎么和服务端连接呢,想要app或者桌面上看服务端上本地传了书", + "labels": null + }, + { + "number": 584, + "title": "建议增加标题的替换功能", + "html_url": "https://github.com/hectorqin/reader/issues/584", + "created_at": "2024-08-29T02:30:02Z", + "closed_at": "2026-09-23T09:15:21Z", + "state_reason": "not_planned", + "comments": 0, + "body": "建议增加标题的替换功能,方便正则去除求月票、求订阅、XX加更之类的内容", + "labels": null + }, + { + "number": 580, + "title": "v3.2.10版本问题汇总", + "html_url": "https://github.com/hectorqin/reader/issues/580", + "created_at": "2024-08-15T13:43:35Z", + "closed_at": "2026-09-23T09:15:21Z", + "state_reason": "not_planned", + "comments": 5, + "body": "1.键盘->下一章出现跳章问题,目前最大跳四章;\r\n2.朗读时候最小化状态切换下一章不是从章头开始朗读(历史版本一直存在此问题);", + "labels": "bug" + }, + { + "number": 573, + "title": "[web]:切换书源界面功能强化-章节关键字搜索以及显示字数", + "html_url": "https://github.com/hectorqin/reader/issues/573", + "created_at": "2024-08-02T09:28:42Z", + "closed_at": "2026-09-23T09:15:21Z", + "state_reason": "not_planned", + "comments": 2, + "body": "\r\n**Describe the bug 描述你遇到的问题**\r\n希望可以象Legado 阅读一样,在切换书源时,可以直接搜索关键字(这样可以搜出已经更新出最新章节的源)并显示章节字数(这样可以知道哪些源是真正更新了该章内容的,那种只有几十个字百来个字的显然不合格)\r\n\r\n以下是Legado阅读和reader相关界面的对比:\r\n![tempImageDPoIDx](https://github.com/user-attachments/assets/6e145a83-0d4e-4d24-890f-1c8ab41ba55a)\r\n![Longshot-20240802172024](https://github.com/user-attachments/assets/4c7289b9-e007-4b27-b8b0-418d1e3436b5)\r\n\r\n**版本说明**\r\n - OS: [e.g. win] MacOS最新\r\n - Deploy Method: Docker\r\n - Program Version:最新 \r\n - Browser [e.g. chrome, safari] MacBook Pro, Safari\r\n\r\n", + "labels": "enhancement" + }, + { + "number": 539, + "title": "希望优化翻页方式(滚动)", + "html_url": "https://github.com/hectorqin/reader/issues/539", + "created_at": "2024-06-04T10:26:30Z", + "closed_at": "2026-09-23T09:15:20Z", + "state_reason": "not_planned", + "comments": 4, + "body": "\r\n翻页方式-滚动翻页,全屏点击-不翻页后。手机触摸滚动屏幕时候偶尔还是会触发整页翻动。希望优化成手机触摸滚动屏幕不要触发页面跳转。", + "labels": null + }, + { + "number": 536, + "title": "希望优化翻页", + "html_url": "https://github.com/hectorqin/reader/issues/536", + "created_at": "2024-06-01T08:30:38Z", + "closed_at": "2026-09-23T09:15:20Z", + "state_reason": "not_planned", + "comments": 4, + "body": "翻页功能能否自定义,不要一点击屏幕就自动跳转翻页。手机使用经常不小心点击到就跳转了其他内容", + "labels": null + }, + { + "number": 523, + "title": "Kindle的浏览器打不开", + "html_url": "https://github.com/hectorqin/reader/issues/523", + "created_at": "2024-05-08T06:01:02Z", + "closed_at": "2026-09-23T09:15:19Z", + "state_reason": "not_planned", + "comments": 5, + "body": "如题\r\n然后在reader里点7天试用,邮箱填了收不到验证码邮件", + "labels": "need-confirm" + }, + { + "number": 529, + "title": "[web/server]: “上下滚动”的翻页方式会导致切换书的时候页面空白", + "html_url": "https://github.com/hectorqin/reader/issues/529", + "created_at": "2024-05-26T20:13:27Z", + "closed_at": "2026-09-23T09:15:19Z", + "state_reason": "not_planned", + "comments": 3, + "body": "使用 “上下滚动”的翻页方式切换书的时候,必然出现三种情况中一种:\r\n1. 点进去的时候当前章节内容部分会变成空白,需要手动在章节列表点击具体章节,或用快捷键切换到下一章才能正常显示。\r\n2. 点进去的时候当前章节内容部分先是依然是上本书的章节内容,卡顿几秒后跳转到新书的某个章节,该章节可能是上上次阅读到的位置,或别的设备阅读到的位置,而不是书架中显示的最新的阅读进度(书架中显示的最新的阅读进度是正确的)。随后书架中显示的最新的阅读进度也变成了这个随机章节。\r\n3. 当前章节内容部分能正常显示,但是章节列表变成空白,也无法用快捷键切换到下一章,需要手动刷新章节列表。\r\n\r\n书均为来自本地书仓的txt文件。\r\n\r\n当选择“上下滑动”翻页方式的时候,则没有任何问题。\r\n\r\n**版本说明**\r\n - OS: 群晖\r\n - Deploy Method: Docker\r\n - Program Version: 3.2.8\r\n - Browser: Safari, macOS\r\n", + "labels": "need-confirm" + }, + { + "number": 519, + "title": "使用nginx反向代理的redaer自带webdav作为阅读3.0的备份服务器,在阅读3.0导入远程书籍时显示超时", + "html_url": "https://github.com/hectorqin/reader/issues/519", + "created_at": "2024-04-30T16:23:36Z", + "closed_at": "2026-09-23T09:15:19Z", + "state_reason": "not_planned", + "comments": 5, + "body": "### 确认 / Assignments\r\n\r\n- [X] 搜索现有issues,不存在相似或相关的issue / No similar or related issues\r\n- [X] 最新[测试版](https://github.com/gedoor/legado/actions/workflows/test.yml)依然存在此问题 / Latest beta app does not work\r\n- [X] 此问题和Xposed、Lsposed、Magisk、手机主题、浏览器插件等无关 / Make sure your machine is not touched by hook frameworks, plugins etc\r\n\r\n### 问题描述 / Describe Bugs\r\n\r\n自己使用docker搭建的reader服务器,使用其自带的webdav作为书库。经过nginx反向代理为子目录访问,访问链接为https://test.com:1122/dav/。可以正常备份和恢复,本地书籍或缓存好的书籍可以正常导入至webdav。在远程书籍中也能够看到目录下的书籍,但是从远程书籍导入时失败,日志显示导入出错,timeout。查看nginx日志,access.log中只有一条记录\"PROPFIND /dav/reader3/webdav/legado/books HTTP/1.1\" 207 22862 \"-\" \"Chrome/119.0.0.0 Safari/537.36 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)\"。error.log中无任何记录。当代理为location /{}时情况一样。\r\n\r\n\r\n我的nginx设置为:\r\nserver {\r\n #添加加ssl和http2协议的要求。\r\n listen 11loca22 ssl ; #ipv4 \r\n #修改为自己申请的域名\r\n server_name test.com; \r\n ###添加秘钥\r\n # Mozilla Intermediate configuration\r\n ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;\r\n #以下协议需要确认硬件是否支持,如果不支持或者不确定可以直接去除\r\n ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; \r\n #以下修改为自己申请的域名公钥和私钥文件地址\r\n ssl_certificate ;\r\n ssl_certificate_key ;\r\n ##############\r\n ssl_session_cache shared:SSL:10m;\r\n ssl_session_timeout 10m; \r\n if ($server_port !~ 1122){\r\n rewrite ^(/.*)$ https://$host$1 permanent;\r\n }\r\n error_page 497 https://$host$request_uri;\r\n\r\nlocation / {\r\n\r\n}\r\n\r\n location /dav/ {\r\n#####\r\n gzip on; #开启gzip压缩\r\n gzip_min_length 1k; #设置对数据启用压缩的最少字节数\r\n gzip_buffers 4 16k;\r\n gzip_http_version 1.0;\r\n gzip_comp_level 6; #设置数据的压缩等级,等级为1-9,压缩比从小到大\r\n gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml; #设置需要压缩的数据格式\r\n gzip_vary on;\r\n client_max_body_size 50m; #允许上传50MB文件,上传本地书籍需要修改此项大小.如nginx主配置文件已添加,删除此行并修改主配置即可\r\n#修改为自己需要代理的局域网ip和端口号\r\n \tproxy_pass http://112.128.1.21:1122/; #阅读 \t\r\n\tproxy_http_version 1.1;\r\n proxy_cache_bypass $http_upgrade;\r\n proxy_set_header Upgrade $http_upgrade;\r\n proxy_set_header Connection \"upgrade\";\r\n proxy_set_header Host $host;\r\n proxy_set_header X-Real-IP $remote_addr;\r\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\r\n proxy_set_header X-Forwarded-Proto $scheme;\r\n proxy_set_header X-Forwarded-Host $host;\r\n proxy_set_header X-Forwarded-Port $server_port;\r\n }\r\n\r\n access_log /var/log/nginx/access.log; #;\r\n error_log /var/log/nginx/error.log; # ; \r\n}\r\n\r\n\r\n\r\n\r\n\r\n### 复现步骤 / How to reproduce\r\n\r\n每次使用都是如此\r\n\r\n### 确认 / Assignment\r\n\r\n- [X] 已经提交复现所需要的附加资料 / Submit additions related with bugs\r\n\r\n### 其他信息 / Additions\r\n\r\n_No response_\r\n\r\n### 日志提交 / Relevant log output\r\n\r\n_No response_\r\n\r\n### 阅读版本 / Legado version\r\n\r\n3.23.110211\r\n\r\n### Android版本 / Android version\r\n\r\nHarmonyOS 4.0.0\r\n\r\n### 机型 / Model\r\n\r\nHUAWEI Mate 40", + "labels": null + }, + { + "number": 513, + "title": "书源失效时服务器缓存不起作用", + "html_url": "https://github.com/hectorqin/reader/issues/513", + "created_at": "2024-04-05T15:38:10Z", + "closed_at": "2026-09-23T09:15:18Z", + "state_reason": "not_planned", + "comments": 1, + "body": "发现书源失效的时候之前浏览过的小说就看不了了,\r\n即使在内容已经缓存在服务器的情况下\r\n![屏幕截图 2024-04-05 233558](https://github.com/hectorqin/reader/assets/31752207/5a2037ae-b60d-4ba6-b44e-92988278e2a0)\r\n在缓存文件夹有内容的情况下,无法设置启用缓存数据代替书源数据", + "labels": null + }, + { + "number": 512, + "title": "竖排书籍无法正常翻页", + "html_url": "https://github.com/hectorqin/reader/issues/512", + "created_at": "2024-04-03T14:01:34Z", + "closed_at": "2026-09-23T09:15:18Z", + "state_reason": "not_planned", + "comments": 1, + "body": "竖排书籍无法正常翻页,只能左右滑动,导致无法存储位置。\r\n点击翻页区域,直接跳到上一章或者下一章。\r\n\r\n以下为提供的竖排epub书籍:\r\n\r\n[越女剑.epub](https://alist.toyqiu.cc:999/d/nas/eBooks/CalibreWeb/Jin%20Yong/Yue%20Nu%20Jian%20(1005)/Yue%20Nu%20Jian%20-%20Jin%20Yong.epub?sign=UPpc4qt_BHHbDRf7MrN1fxDYQQL9EpN5JuR4Ycirf_I=:0)\r\n\r\n[かばん屋の相続.epub](https://alist.toyqiu.cc:999/d/nas/eBooks/CalibreWeb/Chi%20Jing%20Hu%20Run/kabanWu%20noXiang%20Sok%20(1189)/kabanWu%20noXiang%20Sok%20-%20Chi%20Jing%20Hu%20Run.epub?sign=0G2nvkOdPz-yNCGUNCrXiwBo3ZMcrRzv5S4zrlf7IVM=:0)", + "labels": null + }, + { + "number": 498, + "title": "书籍目录无法获取完整", + "html_url": "https://github.com/hectorqin/reader/issues/498", + "created_at": "2024-02-29T12:35:23Z", + "closed_at": "2026-09-23T09:15:17Z", + "state_reason": "not_planned", + "comments": 11, + "body": "**为避免无效问题和冗余问题,提问前请确认**\r\n1. 你确定Google不能解决你的问题 是\r\n2. 你确定已有的issue不能解决你的问题 是\r\n\r\n**Describe the bug 描述你遇到的问题**\r\n\r\n- **部分epub书籍的目录在sigil,calibre-web中可以正常识别目录结构,但是这里并不能完整提取**\r\n\r\n![Snipaste_2024-02-29_20-31-20](https://github.com/hectorqin/reader/assets/52244378/452d28f7-5b87-4f74-8377-a18b1ac4e53e)\r\n![Snipaste_2024-02-29_20-26-53](https://github.com/hectorqin/reader/assets/52244378/b3c5e92d-ae48-402a-a98a-f57199a0c346)\r\n![Snipaste_2024-02-29_20-25-27](https://github.com/hectorqin/reader/assets/52244378/c3974a5f-6e97-404f-b036-972b80c7bcc2)\r\n\r\n**版本说明**\r\n - OS: [e.g. win] 说明操作系统:ubuntu 22.04\r\n - Deploy Method 说明软件部署方式: docker compose\r\n - Program Version 说明软件版本:v3.2.7-01010833\r\n", + "labels": "need-confirm" + }, + { + "number": 491, + "title": "请问怎么设置单次搜索次数?现在需要一直点击加载更多", + "html_url": "https://github.com/hectorqin/reader/issues/491", + "created_at": "2024-02-06T10:39:25Z", + "closed_at": "2026-09-23T09:15:17Z", + "state_reason": "not_planned", + "comments": 2, + "body": "找了下文档和docker的yaml没看到描述此类的配置项,有些书书源多点方便换源,感觉服务器这块,直接检索完全部书源也不是不可以,是否有这个功能呢,有的话在哪里配置呢", + "labels": null + }, + { + "number": 486, + "title": "[server]:有充足内存的情况下报错OutOfMemoryError ", + "html_url": "https://github.com/hectorqin/reader/issues/486", + "created_at": "2024-01-24T13:09:41Z", + "closed_at": "2026-09-23T09:15:16Z", + "state_reason": "not_planned", + "comments": 4, + "body": "**Describe the bug 描述你遇到的问题**\r\n有充足内存的情况下报错OutOfMemoryError ,类似于 #429 ,但明确并非真的发生了内存不足\r\n\r\n**To Reproduce 如何重现问题**\r\nSteps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容:\r\n1. 使用docker运行[reader](https://github.com/hectorqin/reader),并限制内存为2G\r\n2. 对内存占用进行监测\r\n3. 使用10并发进行书源检测\r\n4. 大量请求无返回,达到超时请求后失败;查看日志发现大量报错OutOfMemoryError,但此时仍有充足内存供使用。\r\nlog如:Exception in thread \"DefaultDispatcher-worker-xx\" java.lang.OutOfMemoryError: Java heap space\r\n\r\n**Expected behavior 期待修复的效果**\r\n在用完或接近用完时再报错OutOfMemoryError,方案为增加自定义堆内存大小,如允许在JVM启动时指定参数且不会被脚本覆盖\r\n`-Xmx2048m`,如在下面这行就会和手动设置的内存数冲突。\r\n\r\nhttps://github.com/hectorqin/reader/blob/b3f580f49385376b88f92818b5a9dfa874e1f80f/server/bin/startup.sh#L96\r\n\r\n**Screenshots 如有必要,可以截图说明**\r\n容器内存情况\r\n![image](https://github.com/hectorqin/reader/assets/44089074/9adbb302-5bc5-4672-9cc1-9bbb1e748acb)\r\n宿主机内存情况\r\n![image](https://github.com/hectorqin/reader/assets/44089074/be0427bf-20d3-4878-b7ac-e3bf8dde8a6b)\r\n日志\r\n![image](https://github.com/hectorqin/reader/assets/44089074/b2df2c30-e64a-4035-8612-544bfb71f11b)\r\n\r\n\r\n**版本说明**\r\n - OS: Ubuntu 22.04\r\n - Deploy Method: Docker\r\n - Program Version: `v3.2.7-01010833`\r\n - Browser: Google Chrome v120\r\n", + "labels": null + }, + { + "number": 484, + "title": "书籍列表分页的问题", + "html_url": "https://github.com/hectorqin/reader/issues/484", + "created_at": "2024-01-18T03:25:31Z", + "closed_at": "2026-09-23T09:15:16Z", + "state_reason": "not_planned", + "comments": 5, + "body": "我采用了reader的docker last版本,然后导入了本地9000多本书籍 \r\n当我进入书籍页面后发现,加载书籍列表需要非常长的时间(测试估计在30秒左右) \r\n然后我看了一下请求,发现 getbookshelf接口的确没有进行分页,而是将所有的书籍返回 \r\n\r\n是否有计划下一步进行更新?\r\n\r\n![image](https://github.com/hectorqin/reader/assets/8120401/27901d59-1577-413f-8015-9d80ee78844d)\r\n\r\n![image](https://github.com/hectorqin/reader/assets/8120401/0cbb4687-629f-445d-8c74-9cde7a41fb58)\r\n", + "labels": null + }, + { + "number": 482, + "title": "IOS, web端, 使用翻页的时候, 下一章的内容会跳转到最后,而不是最前,且语音听书的时候经常会有卡住的情况", + "html_url": "https://github.com/hectorqin/reader/issues/482", + "created_at": "2024-01-17T02:01:59Z", + "closed_at": "2026-09-23T09:15:15Z", + "state_reason": "not_planned", + "comments": 2, + "body": "IOS, web端。\r\n1、使用翻页的时候, 偶尔会出现下一章的内容会跳转到最后,而不是最前。\r\n2、语音听书的时候经常会有卡住的情况,需要重新刷新页面才行。\r\n3、web端上下滑动、上下滚动、翻页的时候会存在翻多页的情况,体验不加。\r\n4、IOS使用网页浏览,内置的主题切换会导致页面展示出现问题,比如白天我看到第七页换成夜晚就变成了第2页这种。", + "labels": null + }, + { + "number": 475, + "title": "RSS Feed Bug", + "html_url": "https://github.com/hectorqin/reader/issues/475", + "created_at": "2024-01-03T07:47:17Z", + "closed_at": "2026-09-23T09:15:14Z", + "state_reason": "not_planned", + "comments": 1, + "body": "RSS Feed报错:\r\norg.xmlpull.v1.XmlPullParserException: No valid parser classes found in parameter classNames to newInstance() that contained ' org.kxml2.io.KXmlParser org.kxml2.io.KXmlSerializer '\r\n", + "labels": null + }, + { + "number": 463, + "title": "本地书籍整理功能支持", + "html_url": "https://github.com/hectorqin/reader/issues/463", + "created_at": "2023-11-25T02:46:01Z", + "closed_at": "2026-09-23T09:15:14Z", + "state_reason": "not_planned", + "comments": 1, + "body": "你好,我有1.8G本地书需要整理\r\nQ1:如果在文件夹里多个txt分别为第一章第二章第三章,必须手动合并到一个txt才能识别?能否能增加功能支持\r\n\r\nQ2:本地一个分组就有上百本书籍,能否增加本地搜索支持\r\n\r\nQ3:本地txt添加不同分组导入后,发现他存到了reader\\storage\\data\\default文件夹,但是我本地有1.8G的TXT,也按照分类分好文件夹了,能否出个调用本地文件夹的功能,这样数据就不用再存到reader文件夹下,如果必须存,能否将default文件夹内加上分组文件夹,这样我就把本地txt删了", + "labels": null + }, + { + "number": 454, + "title": "可不可以加入背景透明化", + "html_url": "https://github.com/hectorqin/reader/issues/454", + "created_at": "2023-11-06T06:53:57Z", + "closed_at": "2026-09-23T09:15:14Z", + "state_reason": "not_planned", + "comments": 1, + "body": "类似于这位开发的背景虚化功能\r\nhttps://github.com/binbyu/Reader/blob/master/README.md", + "labels": null + }, + { + "number": 453, + "title": "发一下windows Java安装教程", + "html_url": "https://github.com/hectorqin/reader/issues/453", + "created_at": "2023-11-06T01:12:22Z", + "closed_at": "2026-09-23T09:15:13Z", + "state_reason": "not_planned", + "comments": 0, + "body": "我是小白,之前没接触过需要安装Java的项目,因为安装成功,分享我的步骤。\r\n\r\n1、下载 Java 8 安装包。可以从 Oracle 官网 https://www.java.com/download/ie_manual.jsp 下载。\r\n\r\n2、安装 Java 8。安装过程非常简单,只需要按照屏幕上的提示操作即可。\r\n\r\n3、配置 Java 环境变量。在 Windows 系统中,可以通过以下步骤来配置 Java 环境变量:\r\n\r\n打开控制面板,然后单击“系统”。\r\n在“系统”窗口中,单击“高级系统设置”。\r\n在“高级”选项卡中,单击“环境变量”。\r\n在“系统变量”列表中,单击“新建”。\r\n在“变量名”框中键入“JAVA_HOME”,在“变量值”框中键入 Java 安装目录的路径。例如,如果 Java 安装目录为 C:\\Program Files\\Java\\jdk1.8.0_271,则变量值为 C:\\Program Files\\Java\\jdk1.8.0_271。\r\n单击“确定”。\r\n在“系统变量”列表中,单击“Path”。\r\n在“变量值”框的末尾添加 %JAVA_HOME%\\bin。\r\n单击“确定”。\r\n验证 Java 环境是否安装成功。在命令提示符中输入以下命令:\r\n\r\njava -version\r\n如果显示 Java 版本信息,则表示 Java 环境安装成功。\r\n\r\n需要特别注意的是,在Reader设置,Java路径应当具体到exe文件。列如X:\\Java\\bin\\java.exe", + "labels": null + }, + { + "number": 353, + "title": "WebDAV的疑问", + "html_url": "https://github.com/hectorqin/reader/issues/353", + "created_at": "2023-04-13T14:20:44Z", + "closed_at": "2026-09-23T09:14:41Z", + "state_reason": "completed", + "comments": 27, + "body": "1、阅读APP上WebDAV设置里有个子文件夹,修改这个子文件夹我APP端点备份后会在这个文件夹里生成一个带设备名称的压缩包。而且阅读服务器端点WebDAV保存备份是生成在legado内不带设备名称的压缩包。那服务器端和阅读APP的书籍进度不是存在一个目录内的吗?服务器端好像没有可用修改子文件夹的地方。\r\n2、阅读APP上有个远程书籍,目录是WebDAV内的books文件夹。我在阅读服务器端上把文件上传到这个目录然后加入书架,但阅读APP上充远程书籍上放入书籍一直提示导入出错这是为什么?有什么方法可以把本地书籍放在WebDAV上然后服务器和APP端都能同步进度的吗?\r\n其次必须阅读APP子文件夹目录修改成默认的legado书架才能同步吗?", + "labels": "enhancement" + }, + { + "number": 339, + "title": "为用jb的IDE的程序员们开发了个reader的阅读插件", + "html_url": "https://github.com/hectorqin/reader/issues/339", + "created_at": "2023-03-22T15:39:56Z", + "closed_at": "2026-09-23T09:14:25Z", + "state_reason": "completed", + "comments": 5, + "body": "## 在IDE中随时随地阅读,提升编码效率。\r\n### 同时支持`开源阅读app`和`服务器版阅读`\r\n\r\n都是为了我们程序员能够随时阅读书籍,提升自我\r\n### 点击链接开始阅读:https://github.com/nancheung/legado-reader\r\n\r\n![image](https://user-images.githubusercontent.com/34061813/226959469-82b4485b-ed3c-4db8-95d1-6f215712abbf.png)\r\n\r\n![image](https://user-images.githubusercontent.com/34061813/226958375-7b44e1e3-52ec-48be-ba8e-a3e89b2e27e9.png)\r\n", + "labels": null + }, + { + "number": 720, + "title": "书源功能建议", + "html_url": "https://github.com/hectorqin/reader/issues/720", + "created_at": "2026-09-22T11:36:32Z", + "closed_at": "2026-09-23T07:14:13Z", + "state_reason": "completed", + "comments": 1, + "body": "之前一直使用旧版本reader,在安装了新版本reader 后,书源的使用遇到困难。\n希望出个书源导入的教程。\n\n另外,按照README 来安装,需要修改一下,默认命令无法拉取镜像:\ndocker run -d --name reader \\\n -p 8080:8080 \\\n -v /path/to/your/books:/books:ro \\\n -v ./data:/data \\\n cnb.cool/hectorqin/reader:**main**", + "labels": null + }, + { + "number": 719, + "title": "新版本安装问题及书源导入方法", + "html_url": "https://github.com/hectorqin/reader/issues/719", + "created_at": "2026-09-22T07:34:42Z", + "closed_at": "2026-09-22T08:26:51Z", + "state_reason": "completed", + "comments": 2, + "body": "使用更新后的readme来安装,docker安装时有个问题:拉取不到镜像。使用如下的方式可以拉取\ndocker run -d --name reader \\\n -p 8080:8080 \\\n -v /path/to/your/books:/books:ro \\\n -v ./data:/data \\\n cnb.cool/hectorqin/reader:_main_\n\n另外有个疑问或者说建议:\n老版本的书源导入方式,复制链接后就可以自动同步。\n新版本还不知道怎么导入书源。", + "labels": null + } +] diff --git a/docs/closed-issues-2026-09-23/issues.md b/docs/closed-issues-2026-09-23/issues.md new file mode 100644 index 0000000..cef91ab --- /dev/null +++ b/docs/closed-issues-2026-09-23/issues.md @@ -0,0 +1,1877 @@ +# 最近两天关闭的 Issue 原文存档 + +抓取日期:2026-09-23。共 101 条;按关闭时间筛选,包含历史创建的 issue。正文原样保留;图片和附件未下载。评论仅包含 2026-09-21 起更新的相关评论,不代表完整讨论。 + +## 索引 + +- [#718 会考虑增加书源登陆的功能吗?](https://github.com/hectorqin/reader/issues/718) — 2026/9/23 9:25:14,not_planned +- [#717 希望可以配置导出epub在nas里也存一份,并且可以设置自定义保存目录,另外希望能支持opds](https://github.com/hectorqin/reader/issues/717) — 2026/9/23 9:25:13,not_planned +- [#716 【功能请求】:内容相似度显示](https://github.com/hectorqin/reader/issues/716) — 2026/9/23 9:25:13,not_planned +- [#712 (建议)对于arm版Windows的构建](https://github.com/hectorqin/reader/issues/712) — 2026/9/23 9:25:12,not_planned +- [#714 Docker上用什么字段来修改默认书源上限?](https://github.com/hectorqin/reader/issues/714) — 2026/9/23 9:25:12,not_planned +- [#711 【功能建议】可以自定义网页标题](https://github.com/hectorqin/reader/issues/711) — 2026/9/23 9:25:11,not_planned +- [#710 【功能建议】多级目录](https://github.com/hectorqin/reader/issues/710) — 2026/9/23 9:25:11,not_planned +- [#709 【功能申请】内容划线与注释](https://github.com/hectorqin/reader/issues/709) — 2026/9/23 9:25:10,not_planned +- [#707 会创造大量docker日志,填满磁盘](https://github.com/hectorqin/reader/issues/707) — 2026/9/23 9:25:10,not_planned +- [#706 com.jayway.jsonpath.PathNotFoundException: No results for path: $['data']](https://github.com/hectorqin/reader/issues/706) — 2026/9/23 9:25:09,not_planned +- [#703 能否集成cloudscraper 实现自动过cf盾?](https://github.com/hectorqin/reader/issues/703) — 2026/9/23 9:25:09,not_planned +- [#704 浏览书仓,是否可以搜索子目录?](https://github.com/hectorqin/reader/issues/704) — 2026/9/23 9:25:09,not_planned +- [#702 能够自动加载下一章,每次都得手动点击下一章,特别影响阅读体验](https://github.com/hectorqin/reader/issues/702) — 2026/9/23 9:25:08,not_planned +- [#700 能否适配阅读客户端的webdav同步逻辑](https://github.com/hectorqin/reader/issues/700) — 2026/9/23 9:25:08,not_planned +- [#699 有办法把其他平台的评论抓过来吗](https://github.com/hectorqin/reader/issues/699) — 2026/9/23 9:25:07,not_planned +- [#698 能自动下一章吗?](https://github.com/hectorqin/reader/issues/698) — 2026/9/23 9:25:07,not_planned +- [#696 跳段很影响观感](https://github.com/hectorqin/reader/issues/696) — 2026/9/23 9:25:06,not_planned +- [#697 导入书源过多,直接崩溃,重新打开,书架的书,一直显示报错](https://github.com/hectorqin/reader/issues/697) — 2026/9/23 9:25:06,not_planned +- [#694 【功能建议】建议增加AI速读功能](https://github.com/hectorqin/reader/issues/694) — 2026/9/23 9:25:05,not_planned +- [#695 依赖出现了CVE,建议升级下对应依赖版本](https://github.com/hectorqin/reader/issues/695) — 2026/9/23 9:25:05,not_planned +- [#693 请教如何把reader3服务器做成书源导入阅读apk?](https://github.com/hectorqin/reader/issues/693) — 2026/9/23 9:25:05,not_planned +- [#692 书籍内搜索无法定位](https://github.com/hectorqin/reader/issues/692) — 2026/9/23 9:25:04,not_planned +- [#691 【功能申请】能否在不同的客户端能使用不同的主题?](https://github.com/hectorqin/reader/issues/691) — 2026/9/23 9:25:04,not_planned +- [#690 [web] 朗读功能失效](https://github.com/hectorqin/reader/issues/690) — 2026/9/23 9:25:03,not_planned +- [#689 听书想用第三方的语音要怎么配置啊?](https://github.com/hectorqin/reader/issues/689) — 2026/9/23 9:25:02,not_planned +- [#688 希望修改文章内容是在原txt修改而非重新生成](https://github.com/hectorqin/reader/issues/688) — 2026/9/23 9:17:36,not_planned +- [#684 希望可以锁定横向滑动,添加全屏](https://github.com/hectorqin/reader/issues/684) — 2026/9/23 9:17:35,not_planned +- [#687 docker compose 部署](https://github.com/hectorqin/reader/issues/687) — 2026/9/23 9:17:35,not_planned +- [#677 书海数据响应问题](https://github.com/hectorqin/reader/issues/677) — 2026/9/23 9:17:34,not_planned +- [#679 可以新增功能支持opds协议,来连接自己的书库吗,比如calibre-web?](https://github.com/hectorqin/reader/issues/679) — 2026/9/23 9:17:34,not_planned +- [#674 突然无法打开书架和添加本地书籍,原账号可以登录成功,但原书架数据全都丢失?](https://github.com/hectorqin/reader/issues/674) — 2026/9/23 9:17:33,not_planned +- [#676 [Web] 3.2.14 版本中 “关闭书架虚拟列表优化” 也无法将单列目录还原,且关闭后当书架书籍数量不足时,会拉伸书籍div高度来自适应书架高度,造成界面排版异常](https://github.com/hectorqin/reader/issues/676) — 2026/9/23 9:17:33,not_planned +- [#672 同样的书源手机客户端能搜到书籍,服务器版搜索不到](https://github.com/hectorqin/reader/issues/672) — 2026/9/23 9:17:32,not_planned +- [#673 opds support](https://github.com/hectorqin/reader/issues/673) — 2026/9/23 9:17:32,not_planned +- [#669 求教服务器版本如何看漫画](https://github.com/hectorqin/reader/issues/669) — 2026/9/23 9:17:31,not_planned +- [#670 [web/server] docker compose部署 out of memory](https://github.com/hectorqin/reader/issues/670) — 2026/9/23 9:17:31,not_planned +- [#665 [web/server] claw cloud run 和Railway 部署都失败](https://github.com/hectorqin/reader/issues/665) — 2026/9/23 9:17:30,not_planned +- [#666 部署那个就可以吗](https://github.com/hectorqin/reader/issues/666) — 2026/9/23 9:17:30,not_planned +- [#664 电脑上3.2.14书架和目录的排列](https://github.com/hectorqin/reader/issues/664) — 2026/9/23 9:17:30,not_planned +- [#659 使用自定义目录规则会导致无法调整左右间距(手机模式)](https://github.com/hectorqin/reader/issues/659) — 2026/9/23 9:17:29,not_planned +- [#663 [web/server]:阅读了几章后,点击顶部首页按钮返回书架页后,阅读进度没有刷新,还是显示之前阅读进度,点击进入后也会回到之前的阅读进度](https://github.com/hectorqin/reader/issues/663) — 2026/9/23 9:17:29,not_planned +- [#658 提供 t2s 等新版开源阅读js命令](https://github.com/hectorqin/reader/issues/658) — 2026/9/23 9:17:28,not_planned +- [#657 原先导入的小说,突然在页面中不显示了](https://github.com/hectorqin/reader/issues/657) — 2026/9/23 9:17:28,not_planned +- [#656 [server]macOS下的safari导出书籍时,文件名乱码](https://github.com/hectorqin/reader/issues/656) — 2026/9/23 9:17:27,not_planned +- [#654 目录规则:添加自定义规则以及设置默认规则](https://github.com/hectorqin/reader/issues/654) — 2026/9/23 9:17:27,not_planned +- [#655 服务器版本老是跳,有时候跳段落,有时候跳章节](https://github.com/hectorqin/reader/issues/655) — 2026/9/23 9:17:27,not_planned +- [#651 调试源结果正常,但是在阅读UI界面添加书籍,反应找不到书,怎么回事?](https://github.com/hectorqin/reader/issues/651) — 2026/9/23 9:17:26,not_planned +- [#652 移动端浏览器进度异常跳跃问题【尝试解决中】](https://github.com/hectorqin/reader/issues/652) — 2026/9/23 9:17:26,not_planned +- [#650 请求支持startBrowserAwait去除人机验证](https://github.com/hectorqin/reader/issues/650) — 2026/9/23 9:17:25,not_planned +- [#649 求助:目录能否自动刷新](https://github.com/hectorqin/reader/issues/649) — 2026/9/23 9:17:25,not_planned +- [#647 大佬求助:字符集错误导致文本无法正常显示](https://github.com/hectorqin/reader/issues/647) — 2026/9/23 9:16:52,not_planned +- [#645 [server]翻页模式bug](https://github.com/hectorqin/reader/issues/645) — 2026/9/23 9:16:52,not_planned +- [#644 docker版目录](https://github.com/hectorqin/reader/issues/644) — 2026/9/23 9:16:51,not_planned +- [#642 每次升级版本就会把之前上传的小说重置](https://github.com/hectorqin/reader/issues/642) — 2026/9/23 9:16:51,not_planned +- [#643 过滤规则:是否能够将文本替换为换行](https://github.com/hectorqin/reader/issues/643) — 2026/9/23 9:16:51,not_planned +- [#638 web/server 书架过大的时候会出现超时](https://github.com/hectorqin/reader/issues/638) — 2026/9/23 9:16:50,not_planned +- [#641 [server] 关于自动同步配置造成的问题](https://github.com/hectorqin/reader/issues/641) — 2026/9/23 9:16:50,not_planned +- [#637 每过一段时间,都会自动删除我使用的账号](https://github.com/hectorqin/reader/issues/637) — 2026/9/23 9:16:49,not_planned +- [#636 阅读epub时,iframe和解析模式的bug](https://github.com/hectorqin/reader/issues/636) — 2026/9/23 9:16:49,not_planned +- [#635 在Arc浏览器使用reader的朗读功能,跳到下一章的时候,总是从文章的中间开始。](https://github.com/hectorqin/reader/issues/635) — 2026/9/23 9:16:48,not_planned +- [#632 当使用mongo数据库时,依然会丢失书源](https://github.com/hectorqin/reader/issues/632) — 2026/9/23 9:16:48,not_planned +- [#634 [web/simple]:kindle 的simple-waeb翻页闪屏](https://github.com/hectorqin/reader/issues/634) — 2026/9/23 9:16:48,not_planned +- [#630 从书仓导入PDF文件,大部分页面变成空白](https://github.com/hectorqin/reader/issues/630) — 2026/9/23 9:16:47,not_planned +- [#626 希望把java端整合在setup](https://github.com/hectorqin/reader/issues/626) — 2026/9/23 9:16:47,not_planned +- [#624 翻页。。。在平板上不行啊,上下页不衔接](https://github.com/hectorqin/reader/issues/624) — 2026/9/23 9:16:46,not_planned +- [#622 可以隐藏任务栏吗](https://github.com/hectorqin/reader/issues/622) — 2026/9/23 9:16:46,not_planned +- [#623 [web]:IOS设备使用safari等webkit内核浏览器在最新版本系统中无法使用大声朗读\httpTTS的tts](https://github.com/hectorqin/reader/issues/623) — 2026/9/23 9:16:46,not_planned +- [#618 删除书籍后,assets目录下的export与cover未删除](https://github.com/hectorqin/reader/issues/618) — 2026/9/23 9:16:45,not_planned +- [#616 大声朗读局域网IP可以使用,使用花生壳域名访问无法使用 DMZ主机是开着的](https://github.com/hectorqin/reader/issues/616) — 2026/9/23 9:16:45,not_planned +- [#613 分享一个自定义Docker TTS 语音项目 可配 web TTS 朗读](https://github.com/hectorqin/reader/issues/613) — 2026/9/23 9:16:44,not_planned +- [#614 数据库+redis方案](https://github.com/hectorqin/reader/issues/614) — 2026/9/23 9:16:44,not_planned +- [#608 服务器版手机 edge 浏览器自带大声朗读无法播放](https://github.com/hectorqin/reader/issues/608) — 2026/9/23 9:16:43,not_planned +- [#601 添加本地书籍报错 获取章节内容失败! 章节文件不存在](https://github.com/hectorqin/reader/issues/601) — 2026/9/23 9:16:43,not_planned +- [#596 页面显示的章节与打开书籍后显示的章节不一致](https://github.com/hectorqin/reader/issues/596) — 2026/9/23 9:16:43,not_planned +- [#594 【建议】希望编辑后可以永久生效,而不是刷新后就会失效](https://github.com/hectorqin/reader/issues/594) — 2026/9/23 9:16:42,not_planned +- [#590 请教下各位书友httptts 怎么配置](https://github.com/hectorqin/reader/issues/590) — 2026/9/23 9:15:23,not_planned +- [#585 是否能修改下翻页的一个问题,章尾最后一页的排版,最后一行字始终是在页面最底部](https://github.com/hectorqin/reader/issues/585) — 2026/9/23 9:15:22,not_planned +- [#588 windows桌面版和安卓app版怎么和服务端连接](https://github.com/hectorqin/reader/issues/588) — 2026/9/23 9:15:22,not_planned +- [#584 建议增加标题的替换功能](https://github.com/hectorqin/reader/issues/584) — 2026/9/23 9:15:21,not_planned +- [#580 v3.2.10版本问题汇总](https://github.com/hectorqin/reader/issues/580) — 2026/9/23 9:15:21,not_planned +- [#573 [web]:切换书源界面功能强化-章节关键字搜索以及显示字数](https://github.com/hectorqin/reader/issues/573) — 2026/9/23 9:15:21,not_planned +- [#539 希望优化翻页方式(滚动)](https://github.com/hectorqin/reader/issues/539) — 2026/9/23 9:15:20,not_planned +- [#536 希望优化翻页](https://github.com/hectorqin/reader/issues/536) — 2026/9/23 9:15:20,not_planned +- [#523 Kindle的浏览器打不开](https://github.com/hectorqin/reader/issues/523) — 2026/9/23 9:15:19,not_planned +- [#529 [web/server]: “上下滚动”的翻页方式会导致切换书的时候页面空白](https://github.com/hectorqin/reader/issues/529) — 2026/9/23 9:15:19,not_planned +- [#519 使用nginx反向代理的redaer自带webdav作为阅读3.0的备份服务器,在阅读3.0导入远程书籍时显示超时](https://github.com/hectorqin/reader/issues/519) — 2026/9/23 9:15:19,not_planned +- [#513 书源失效时服务器缓存不起作用](https://github.com/hectorqin/reader/issues/513) — 2026/9/23 9:15:18,not_planned +- [#512 竖排书籍无法正常翻页](https://github.com/hectorqin/reader/issues/512) — 2026/9/23 9:15:18,not_planned +- [#498 书籍目录无法获取完整](https://github.com/hectorqin/reader/issues/498) — 2026/9/23 9:15:17,not_planned +- [#491 请问怎么设置单次搜索次数?现在需要一直点击加载更多](https://github.com/hectorqin/reader/issues/491) — 2026/9/23 9:15:17,not_planned +- [#486 [server]:有充足内存的情况下报错OutOfMemoryError ](https://github.com/hectorqin/reader/issues/486) — 2026/9/23 9:15:16,not_planned +- [#484 书籍列表分页的问题](https://github.com/hectorqin/reader/issues/484) — 2026/9/23 9:15:16,not_planned +- [#482 IOS, web端, 使用翻页的时候, 下一章的内容会跳转到最后,而不是最前,且语音听书的时候经常会有卡住的情况](https://github.com/hectorqin/reader/issues/482) — 2026/9/23 9:15:15,not_planned +- [#475 RSS Feed Bug](https://github.com/hectorqin/reader/issues/475) — 2026/9/23 9:15:14,not_planned +- [#463 本地书籍整理功能支持](https://github.com/hectorqin/reader/issues/463) — 2026/9/23 9:15:14,not_planned +- [#454 可不可以加入背景透明化](https://github.com/hectorqin/reader/issues/454) — 2026/9/23 9:15:14,not_planned +- [#453 发一下windows Java安装教程](https://github.com/hectorqin/reader/issues/453) — 2026/9/23 9:15:13,not_planned +- [#353 WebDAV的疑问](https://github.com/hectorqin/reader/issues/353) — 2026/9/23 9:14:41,completed +- [#339 为用jb的IDE的程序员们开发了个reader的阅读插件](https://github.com/hectorqin/reader/issues/339) — 2026/9/23 9:14:25,completed +- [#720 书源功能建议](https://github.com/hectorqin/reader/issues/720) — 2026/9/23 7:14:13,completed +- [#719 新版本安装问题及书源导入方法](https://github.com/hectorqin/reader/issues/719) — 2026/9/22 8:26:51,completed + +## #718 会考虑增加书源登陆的功能吗? + +原始链接:https://github.com/hectorqin/reader/issues/718 +创建:2026/6/6 13:42:08;关闭:2026/9/23 9:25:14;原因:not_planned;历史评论总数:0。时间为 UTC。 + +现在好的书源都要会员和登陆才行。 + +## #717 希望可以配置导出epub在nas里也存一份,并且可以设置自定义保存目录,另外希望能支持opds + +原始链接:https://github.com/hectorqin/reader/issues/717 +创建:2026/5/27 15:37:20;关闭:2026/9/23 9:25:13;原因:not_planned;历史评论总数:0。时间为 UTC。 + +(无正文) + +## #716 【功能请求】:内容相似度显示 + +原始链接:https://github.com/hectorqin/reader/issues/716 +创建:2026/5/19 2:06:39;关闭:2026/9/23 9:25:13;原因:not_planned;历史评论总数:0。时间为 UTC。 + +大佬好,请问能否出一个功能,当切换书源的时候,在书源每行的延迟时间旁边添加两个内容,一个是该书源在本章的字数,另一个是该书源在和本书源在本章节的相似度(可以直接借用notepad++的compare功能)。 +这样如果在读书的时候,这个书源的内容被修改了,可以快速的找到正确的书源内容,不用再一个一个更换了 + +## #712 (建议)对于arm版Windows的构建 + +原始链接:https://github.com/hectorqin/reader/issues/712 +创建:2026/2/2 14:50:50;关闭:2026/9/23 9:25:12;原因:not_planned;历史评论总数:0。时间为 UTC。 + +希望可以构建Windows的arm版本 + + + +## #714 Docker上用什么字段来修改默认书源上限? + +原始链接:https://github.com/hectorqin/reader/issues/714 +创建:2026/4/20 1:50:37;关闭:2026/9/23 9:25:12;原因:not_planned;历史评论总数:1。时间为 UTC。 + +defaultUserBookSourceLimit修改到2000是无效的,最高100。用什么字段来修改默认书源上限? + +## #711 【功能建议】可以自定义网页标题 + +原始链接:https://github.com/hectorqin/reader/issues/711 +创建:2026/1/10 1:04:00;关闭:2026/9/23 9:25:11;原因:not_planned;历史评论总数:1。时间为 UTC。 + +希望可以自定义网页标题,现在默认是小说名字+章节,如果能自定义就能更隐蔽的阅读。 + +## #710 【功能建议】多级目录 + +原始链接:https://github.com/hectorqin/reader/issues/710 +创建:2026/1/6 16:23:25;关闭:2026/9/23 9:25:11;原因:not_planned;历史评论总数:0。时间为 UTC。 + +想要多级目录 + +## #709 【功能申请】内容划线与注释 + +原始链接:https://github.com/hectorqin/reader/issues/709 +创建:2026/1/6 15:33:57;关闭:2026/9/23 9:25:10;原因:not_planned;历史评论总数:0。时间为 UTC。 + +想要这个功能,可能对文字内容进行划线,并可以选择对其进行文字注释。用的 docker 版本,没找到这种操作,不知道桌面端有没有 + +## #707 会创造大量docker日志,填满磁盘 + +原始链接:https://github.com/hectorqin/reader/issues/707 +创建:2025/12/26 6:31:58;关闭:2026/9/23 9:25:10;原因:not_planned;历史评论总数:1。时间为 UTC。 + +每天都会创建大量日志,导致磁盘被空间 + +## #706 com.jayway.jsonpath.PathNotFoundException: No results for path: $['data'] + +原始链接:https://github.com/hectorqin/reader/issues/706 +创建:2025/12/25 16:43:35;关闭:2026/9/23 9:25:09;原因:not_planned;历史评论总数:0。时间为 UTC。 + +不管什么书源加入书全是这个报错 +com.jayway.jsonpath.PathNotFoundException: No results for path: $['data'] + +## #703 能否集成cloudscraper 实现自动过cf盾? + +原始链接:https://github.com/hectorqin/reader/issues/703 +创建:2025/12/8 13:11:57;关闭:2026/9/23 9:25:09;原因:not_planned;历史评论总数:0。时间为 UTC。 + +能否集成 https://github.com/VeNoMouS/cloudscraper ,实现自动过书源站点的cf盾,例如https://www.69shuba.com/ 这种优质书源站点挂了cf盾,不能自动过盾的话,就没法搜索阅读。 +谢谢! + +## #704 浏览书仓,是否可以搜索子目录? + +原始链接:https://github.com/hectorqin/reader/issues/704 +创建:2025/12/16 13:47:58;关闭:2026/9/23 9:25:09;原因:not_planned;历史评论总数:0。时间为 UTC。 + +我的本地书仓按照书籍类型有几个子目录,导入本地书仓时,搜索功能无法搜索子目录 + +## #702 能够自动加载下一章,每次都得手动点击下一章,特别影响阅读体验 + +原始链接:https://github.com/hectorqin/reader/issues/702 +创建:2025/12/8 1:14:22;关闭:2026/9/23 9:25:08;原因:not_planned;历史评论总数:3。时间为 UTC。 + +我是nas部署的,然后用手机浏览器阅读,每次音量键翻页到章节末页都得手动点击下一章,特别影响阅读体验 + +## #700 能否适配阅读客户端的webdav同步逻辑 + +原始链接:https://github.com/hectorqin/reader/issues/700 +创建:2025/11/15 5:03:21;关闭:2026/9/23 9:25:08;原因:not_planned;历史评论总数:0。时间为 UTC。 + +实现自动同步,而非需要手动点击还原 + +## #699 有办法把其他平台的评论抓过来吗 + +原始链接:https://github.com/hectorqin/reader/issues/699 +创建:2025/11/8 9:48:14;关闭:2026/9/23 9:25:07;原因:not_planned;历史评论总数:0。时间为 UTC。 + +如题 + +## #698 能自动下一章吗? + +原始链接:https://github.com/hectorqin/reader/issues/698 +创建:2025/11/6 11:29:25;关闭:2026/9/23 9:25:07;原因:not_planned;历史评论总数:0。时间为 UTC。 + +每一章读到最后,能自动加载下一章吗?不需要手动点击才能下一章。 + +## #696 跳段很影响观感 + +原始链接:https://github.com/hectorqin/reader/issues/696 +创建:2025/10/17 10:18:49;关闭:2026/9/23 9:25:06;原因:not_planned;历史评论总数:0。时间为 UTC。 + +把跳段修修吧,求你了很影响看书 + +## #697 导入书源过多,直接崩溃,重新打开,书架的书,一直显示报错 + +原始链接:https://github.com/hectorqin/reader/issues/697 +创建:2025/11/3 3:01:47;关闭:2026/9/23 9:25:06;原因:not_planned;历史评论总数:1。时间为 UTC。 + +一直出现下面的报错,io.vertx.core.json.DecodeException: Failed to decode:No content to map due to end-of-input at [Source: (String)""; line: 1, column: 0] + +## #694 【功能建议】建议增加AI速读功能 + +原始链接:https://github.com/hectorqin/reader/issues/694 +创建:2025/9/11 14:02:44;关闭:2026/9/23 9:25:05;原因:not_planned;历史评论总数:0。时间为 UTC。 + +建议增加AI总结功能,可通过大模型API对每章内容进行总结提炼,帮助快速阅读 + +## #695 依赖出现了CVE,建议升级下对应依赖版本 + +原始链接:https://github.com/hectorqin/reader/issues/695 +创建:2025/10/14 19:02:18;关闭:2026/9/23 9:25:05;原因:not_planned;历史评论总数:0。时间为 UTC。 + +**Describe the bug 描述你遇到的问题** +FasterXML jackson-databind远程代码执行漏洞(CVE-2019-17531) +在3.2.14 jar包中扫描出了有问题版本的依赖,建议升级下依赖版本。 + + +## #693 请教如何把reader3服务器做成书源导入阅读apk? + +原始链接:https://github.com/hectorqin/reader/issues/693 +创建:2025/8/28 11:36:02;关闭:2026/9/23 9:25:05;原因:not_planned;历史评论总数:7。时间为 UTC。 + +如标题所述,起因需求是reader3服务器能正常访问海外,而手机毕竟无法长期上T,如果能把服务器做成书源导入手机阅读apk,那不就方便了么? +请高手指点一下如何操作? + +## #692 书籍内搜索无法定位 + +原始链接:https://github.com/hectorqin/reader/issues/692 +创建:2025/8/24 8:22:09;关闭:2026/9/23 9:25:04;原因:not_planned;历史评论总数:0。时间为 UTC。 + +作者您好,我在使用这个软件阅读时,我在书籍内搜索关键字,会列出所有结果,我点击一个结果,然后只会跳转到结果所在的那一章的开头而不是到这个结果的具体位置。我尝试了使用单页模式(切换成手机模式再用左右翻面)还是一样的情况。是我没有设置对还是这是一个bug? + +## #691 【功能申请】能否在不同的客户端能使用不同的主题? + +原始链接:https://github.com/hectorqin/reader/issues/691 +创建:2025/8/23 8:42:14;关闭:2026/9/23 9:25:04;原因:not_planned;历史评论总数:0。时间为 UTC。 + +不同的设备要使用不同的主题,电脑,手机,电纸书(还分彩色和黑白)等等要使用不同的主题才更适合。 + +客户端做本地标识,然后再后台列表记录一下应该就可以了。或者直接把主题选择存在本地更简单。 + + + +另外,还有个问题,打开页面时读书籍列表很废时,体验不好,能不能在本地缓存之前的,打开时先显示缓存,更新后再前台刷新,这样体验应该会更好。 + +## #690 [web] 朗读功能失效 + +原始链接:https://github.com/hectorqin/reader/issues/690 +创建:2025/8/20 4:59:48;关闭:2026/9/23 9:25:03;原因:not_planned;历史评论总数:4。时间为 UTC。 + +**为避免无效问题和冗余问题,提问前请确认** +1. 你确定Google不能解决你的问题 +2. 你确定已有的issue不能解决你的问题 +3. 你确定issue的title按照格式如下:[web/simple-web/server]:description + +**Describe the bug 描述你遇到的问题** +朗读功能突然失效, 包含`浏览器`和 `大声朗读` + +**To Reproduce 如何重现问题** +Steps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容: +1. 打开小说 +2. 点开朗读功能 +3. 任意朗读服务 +4. 点击播放 + +**Expected behavior 期待修复的效果** +播放朗读 + +**Screenshots 如有必要,可以截图说明** +- + +**版本说明** + - OS: 树莓pi 4, ubuntu + - docker 部署 + - hectorqin/reader:openj9-latest + - chrome 浏览器(桌面端 & 手机端) + +**Additional context 其他说明** +检查 api 返回内容, 并没有资料, 返回内容如下: +```json +{"isSuccess":true,"errorMsg":"","data":""} +``` + + +## #689 听书想用第三方的语音要怎么配置啊? + +原始链接:https://github.com/hectorqin/reader/issues/689 +创建:2025/8/19 11:52:39;关闭:2026/9/23 9:25:02;原因:not_planned;历史评论总数:4。时间为 UTC。 + +我想用听书功能,但默认的语音太难听了,想自己配置第三方语音但不知道怎么配,这个HttpTTS是用来配置第三方的吗?文档里也没有说明。 + +Image + +## #688 希望修改文章内容是在原txt修改而非重新生成 + +原始链接:https://github.com/hectorqin/reader/issues/688 +创建:2025/8/13 9:18:59;关闭:2026/9/23 9:17:36;原因:not_planned;历史评论总数:1。时间为 UTC。 + + 目前编辑文章内容是重新生成一份新的txt,包括从书仓导入的,这样导致编辑的内容只是在前端web显示,书仓的原书籍依然是不变的。 + 并且一旦刷新更改的内容就全部消失,但是后端新建的txt又还在,这样的操作逻辑比较奇怪。 + 不清楚实现这样的功能是否过于复杂,如果难以实现的话也只能通过手动修改书仓内txt进行变更,就是比较麻烦,主要是因为部分文章存在缺文少字的情况,难免会有需要手动更改,但是目前的修改功能感觉并不能很好的支撑这一需求。 + +Image + +## #684 希望可以锁定横向滑动,添加全屏 + +原始链接:https://github.com/hectorqin/reader/issues/684 +创建:2025/8/5 7:42:55;关闭:2026/9/23 9:17:35;原因:not_planned;历史评论总数:0。时间为 UTC。 + +1. 我在使用触控板进行滑动阅读的时候,有时候会略微偏斜导致先横向滑动,后竖向滚动,发生页面晃动,影响阅读体验。我试了一下各个浏览器,发现似乎只有谷歌浏览器是横向滑动的时候页面不动,希望可以锁定横向滑动。 + +2. 希望添加类似浏览器f11的全屏功能 + + +## #687 docker compose 部署 + +原始链接:https://github.com/hectorqin/reader/issues/687 +创建:2025/8/12 9:27:10;关闭:2026/9/23 9:17:35;原因:not_planned;历史评论总数:3。时间为 UTC。 + +每次都是从第一章开始,没有获取阅读进度 + + +## #677 书海数据响应问题 + +原始链接:https://github.com/hectorqin/reader/issues/677 +创建:2025/6/30 2:18:46;关闭:2026/9/23 9:17:34;原因:not_planned;历史评论总数:0。时间为 UTC。 + +**为避免无效问题和冗余问题,提问前请确认** +1. 你确定Google不能解决你的问题 +2. 你确定已有的issue不能解决你的问题 +3. 你确定issue的title按照格式如下:[web/simple-web/server]:description + +**Describe the bug 描述你遇到的问题** +A clear and concise description of what the bug is. 简洁有效的说明。 +书海哪里点击响应问题 + +**To Reproduce 如何重现问题** +Steps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容: +1. Go to '...' +2. + +![Image](https://github.com/user-attachments/assets/c3bbb451-fef5-434e-8daa-9077790a67cc) +点击了 但是没有数据请求 +3. Click on '....' +4. 点击下一个选项有了上一个的数据请求并响应回来 + +![Image](https://github.com/user-attachments/assets/424f299c-70aa-4e0e-bec5-841cba1ac9b5) + +5. Scroll down to '....' +6. See error**Expected behavior 期待修复的效果** +A clear and concise description of what you expected to happen. 简单描述。 + +**Screenshots 如有必要,可以截图说明** +If applicable, add screenshots to help explain your problem. + +**版本说明** + - OS: [e.g. win] win + - Deploy Method web版 + - Program Version 3.2.14 + - Browser [e.g. chrome, safari] edge + +**Additional context 其他说明** +Add any other context about the problem here. 添加你认为有必要的内容,否则不写。 + + +## #679 可以新增功能支持opds协议,来连接自己的书库吗,比如calibre-web? + +原始链接:https://github.com/hectorqin/reader/issues/679 +创建:2025/7/4 4:08:51;关闭:2026/9/23 9:17:34;原因:not_planned;历史评论总数:1。时间为 UTC。 + +目前自己同时也部署了一套calibre-web,希望可以通过reader3来连接calibre-web来阅读自己本地的书籍,reader3本身自带的本地书仓不太好用,默认只能放到localstorage下,挂载了其他目录也访问不到。 + +## #674 突然无法打开书架和添加本地书籍,原账号可以登录成功,但原书架数据全都丢失? + +原始链接:https://github.com/hectorqin/reader/issues/674 +创建:2025/6/18 8:27:17;关闭:2026/9/23 9:17:33;原因:not_planned;历史评论总数:0。时间为 UTC。 + +**为避免无效问题和冗余问题,提问前请确认** +1. 你确定Google不能解决你的问题 +2. 你确定已有的issue不能解决你的问题 +3. 你确定issue的title按照格式如下:[web/simple-web/server]:description + +**Describe the bug 描述你遇到的问题** +A clear and concise description of what the bug is. 简洁有效的说明。 +1、我是在群晖上用docker架设的阅读服务器(多用户模式),长期使用域名访问看书起码半年多了。今天群晖突然故障重启,然后阅读服务器下我个人常用的账号书架书籍全消失了,浏览书仓功能可以进入,但要重新添加书仓内的本地txt小说却会失败,会提示:“io.vertx.core,json.DecodeException: Failed to decode:No content to map due to end-of input at [Source: ($tring)"; line: 1, column: 0]”;该账号登录是会有提示“io.vertx.corejson.DecodeException: Failed to decode:No content to map due to end-of-input at [Source:(String)"; line: 1, column: 0]”两次及提示“没有备份文件”。如图所示。 +奇怪的是,用其他账号登录,书架也在,添加书籍操作也在。 +2、我访问服务用的是网页版,但是在PC端chrome浏览器下访问,关键词搜索新书籍时会搜不到书,要在手机端chrome浏览器下就可以搜索到到书籍,明明是同样的书源。 +**To Reproduce 如何重现问题** +Steps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior 期待修复的效果** +A clear and concise description of what you expected to happen. 简单描述。 +希望咨询账号数据恢复的手段。 +**Screenshots 如有必要,可以截图说明** +If applicable, add screenshots to help explain your problem. + +![Image](https://github.com/user-attachments/assets/79959a1c-5fd1-49b4-9557-a818e0d80a60) + +![Image](https://github.com/user-attachments/assets/396ecdfc-e582-4f9e-a36c-c8536077eb92) + +**版本说明** + - OS: 群晖DSM + - Deploy Method docker部署 + - Program Version最新的3.2.14 + - Browser [e.g. chrome, safari] chrome浏览器,PC端 + +**Additional context 其他说明** +Add any other context about the problem here. 添加你认为有必要的内容,否则不写。 + + +## #676 [Web] 3.2.14 版本中 “关闭书架虚拟列表优化” 也无法将单列目录还原,且关闭后当书架书籍数量不足时,会拉伸书籍div高度来自适应书架高度,造成界面排版异常 + +原始链接:https://github.com/hectorqin/reader/issues/676 +创建:2025/6/28 3:14:32;关闭:2026/9/23 9:17:33;原因:not_planned;历史评论总数:13。时间为 UTC。 + +**Describe the bug 描述你遇到的问题** +1、更新 `3.2.14` 版本 [reader-pro-3.2.14.jar](https://github.com/hectorqin/reader/releases/download/v3.2.14/reader-pro-3.2.14.jar) 并重新启动后,默认启用了 `书架虚拟列表优化` ,界面显示单列但无异常; +2、在左侧 `书架设置` --> `列表优化` 中选择 `不启用优化` 后保存,因为曾经为之前的版本为书籍的 info div 块添加了 `height: auto;min-height: 112px;`,因此发现在更新错误分组中书籍高度显示异常,经检查发现即使去掉了添加的 css 样式,凡是分组中书籍数量过少的界面,书籍的 div 块高度都会默认拉伸铺满分组书架界面; +3、即使在 “关闭书架虚拟列表优化” 后,进入书籍,目录列表显示也是单列,没有找到可以调整的设置,无法调整。试过重置后重新查看、备份后重新查看均无效。 + +**Expected behavior 期待修复的效果** +希望书籍目录列表可以正常切换单列和多列;书籍少的分组界面显示优化。 + +**Screenshots 如有必要,可以截图说明** +1、`启用虚拟列表优化` 时界面显示 +Image +2、`不启用优化` 时书架分组界面显示 +【添加 css 样式】 +Image +【去除 css 样式】 +Image +3、`不启用优化` 时书籍列表界面显示 +Image + +**版本说明** + - OS: Windows 11 + - Deploy Method: [reader-pro-3.2.14.jar](https://github.com/hectorqin/reader/releases/download/v3.2.14/reader-pro-3.2.14.jar) + - Program Version: 3.2.14 + - Browser: edge 浏览器 + + +## #672 同样的书源手机客户端能搜到书籍,服务器版搜索不到 + +原始链接:https://github.com/hectorqin/reader/issues/672 +创建:2025/6/17 1:55:40;关闭:2026/9/23 9:17:32;原因:not_planned;历史评论总数:5。时间为 UTC。 + +手机上用的阅读3客户端,在云服务器上也部署了3.2.14版本。手机和服务器上的书源是同一套,但是同样的小说在阅读3上能搜到,在服务器版上可能就搜不到。而且搜索结果的数量也是少很多。我会调的只有并发线程,把线程调高了也没什么用。 + +## #673 opds support + +原始链接:https://github.com/hectorqin/reader/issues/673 +创建:2025/6/17 11:27:19;关闭:2026/9/23 9:17:32;原因:not_planned;历史评论总数:1。时间为 UTC。 + +大佬目前版本支持opds么 + +## #669 求教服务器版本如何看漫画 + +原始链接:https://github.com/hectorqin/reader/issues/669 +创建:2025/6/4 16:53:49;关闭:2026/9/23 9:17:31;原因:not_planned;历史评论总数:2。时间为 UTC。 + +提示不支持webview或者显示空白 + +![Image](https://github.com/user-attachments/assets/a2ac2179-90c3-4493-8991-2f572dfc7170) + + + +## #670 [web/server] docker compose部署 out of memory + +原始链接:https://github.com/hectorqin/reader/issues/670 +创建:2025/6/5 4:00:30;关闭:2026/9/23 9:17:31;原因:not_planned;历史评论总数:1。时间为 UTC。 + +**为避免无效问题和冗余问题,提问前请确认** +1. 你确定Google不能解决你的问题 +2. 你确定已有的issue不能解决你的问题 +3. 你确定issue的title按照格式如下:[web/simple-web/server]:description + +**Describe the bug 描述你遇到的问题** + docker compose部署 docker启动时报错 +``` +library initialization failed - unable to allocate file descriptor table - out of memory11:49:36.914 [main] INFO com.htmake.reader.ReaderApplication - Starting application with args: [] +``` + 机器内存16G +``` +dnine7@dnine7-GL63-8RE:~/reader$ free -h + total used free shared buff/cache available +内存: 14Gi 4.9Gi 9.3Gi 265Mi 1.3Gi 10Gi +交换: 4.0Gi 0B 4.0Gi +``` + +**版本说明** + - OS: [e.g. win] 说明操作系统 ubuntu 24.10 + - Deploy Method 说明软件部署方式 docker compose + - Program Version 说明软件版本 3.2.14 + +**docker-compose-yaml** +``` +version: '3.1' +services: +# reader 在线阅读 +# 公开服务器(服务器位于日本):[https://reader.nxnow.top](https://reader.nxnow.top) 测试账号/密码分别为guest/guest123,也可自行创建账号添加书源,不定期删除长期未登录账号(2周) +# 书源集合 : [https://legado.aoaostar.com/](https://legado.aoaostar.com/) 点击打开连接,添加远程书源即可 +# 公众号汇总 : [https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MzMyMDgyMA==&action=getalbum&album_id=2397535253763801090#wechat_redirect](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MzMyMDgyMA==&action=getalbum&album_id=2397535253763801090#wechat_redirect) +# 手动更新方式 : docker-compose pull && docker-compose up -d + reader: + image: hectorqin/reader:latest + #image: hectorqin/reader:openj9-latest #docker镜像,arm64架构或小内存机器优先使用此镜像.启用需删除上一行 + container_name: reader #容器名 可自行修改 + restart: always + ports: + - 9970:8080 #4396端口映射可自行修改 + networks: + - share_net + volumes: + - /home/dnine7/reader/logs:/logs #log映射目录 /home/reader/logs 映射目录可自行修改 + - /home/dnine7/reader/storage:/storage #数据映射目录 /home/reader/storage 映射目录可自行修改 + environment: + - SPRING_PROFILES_ACTIVE=prod + - READER_APP_USERLIMIT=999999 #用户上限,默认50 + - READER_APP_USERBOOKLIMIT=99999 #用户书籍上限,默认200 + - READER_APP_CACHECHAPTERCONTENT=true #开启缓存章节内容 V2.0 + # 如果启用远程webview,需要取消注释下面的 remote-webview 服务 + # - READER_APP_REMOTEWEBVIEWAPI=http://remote-webview:8050 #开启远程webview + # 下面都是多用户模式配置 + - READER_APP_SECURE=true #开启登录鉴权,开启后将支持多用户模式 + - READER_APP_SECUREKEY=xzq521xzq #管理员密码 建议修改 + - READER_APP_INVITECODE=Dnine7 #注册邀请码 建议修改,如不需要可注释或删除 + # remote-webview: + # image: hectorqin/remote-webview + # container_name: remote-webview #容器名 可自行修改 + # restart: always + # ports: + # - 8050:8050 + # networks: + # - share_net +# 自动更新docker镜像 + watchtower: + image: containrrr/watchtower + container_name: watchtower + restart: always + # 环境变量,设置为上海时区 + environment: + - TZ=Asia/Shanghai + volumes: + - /var/run/docker.sock:/var/run/docker.sock + command: reader watchtower --cleanup --schedule "0 0 4 * * *" + networks: + - share_net + # 仅更新reader与watchtower容器,如需其他自行添加 '容器名' ,如:reader watchtower nginx + # --cleanup 更新后清理旧版本镜像 + # --schedule 自动检测更新 crontab定时(限定6位crontab) 此处代表凌晨4点整 +networks: + share_net: + driver: bridge +``` + + +## #665 [web/server] claw cloud run 和Railway 部署都失败 + +原始链接:https://github.com/hectorqin/reader/issues/665 +创建:2025/5/30 13:35:48;关闭:2026/9/23 9:17:30;原因:not_planned;历史评论总数:1。时间为 UTC。 + +![Image](https://github.com/user-attachments/assets/3df583a0-cdec-4d39-a5e6-187048f60d00) + +![Image](https://github.com/user-attachments/assets/3eb8f049-5c62-4c81-827e-f90c3c940ae7) + +![Image](https://github.com/user-attachments/assets/bac806fb-762c-4ffe-86be-2e0e97034ec8) + +部署方式:https://mp.weixin.qq.com/s/TCL6i_7Zrgf2okJG-BF07Q +如果有成功的大佬请帮帮忙看一下,是怎么回事?要怎么处理,还是说当前的这个版本是不能部署的?能部署的是哪个版本,有没有对应的链接, + +感谢各位大佬!!! + +## #666 部署那个就可以吗 + +原始链接:https://github.com/hectorqin/reader/issues/666 +创建:2025/5/30 14:02:54;关闭:2026/9/23 9:17:30;原因:not_planned;历史评论总数:0。时间为 UTC。 + +> Reader 完整源码仅开放到 v2.5.4,新版本当前仅开放部分开源源码,见 https://github.com/hectorqin/reader-legado. +> +> +> +> @jueinin +> +> 早就是闭源的了 + + _Originally posted by @rolay in [#498](https://github.com/hectorqin/reader/issues/498#issuecomment-2313894063)_ + +那意思是部署上面的这个就行吗? https://github.com/hectorqin/reader-legado. + +## #664 电脑上3.2.14书架和目录的排列 + +原始链接:https://github.com/hectorqin/reader/issues/664 +创建:2025/5/29 23:47:13;关闭:2026/9/23 9:17:30;原因:not_planned;历史评论总数:2。时间为 UTC。 + +电脑上3.2.14书架和目录的排列都是类似手机的单列竖排,没有哪里可以设置成跟以前一样的吗 + +书架左边栏找到了书架设置,目录没找到。 + +但是书架,如果设置成网格显示,就只显示封面,不显示已读和最新章节,且无论点击还是长按都是直接点进小说里,右键无效,也就是无法像列表模式点击封面那样出现编辑,移出书架,封面设置等选项。 +像保留这些选项就只能设置成单列的列表模式,希望可以保留跟以前的书架一样的多列列表的模式。另外就是目录的多列显示哪里设置 + +## #659 使用自定义目录规则会导致无法调整左右间距(手机模式) + +原始链接:https://github.com/hectorqin/reader/issues/659 +创建:2025/5/14 17:59:39;关闭:2026/9/23 9:17:29;原因:not_planned;历史评论总数:0。时间为 UTC。 + +使用自定义目录规则会导致部分小说无法调整左右间距 + +小说来源:本地txt + +版本:reader-pro-3.2.13.jar + +问题出现在web手机模式。 + +使用的规则是自定义的冒号分割 + +正则表达式:`.*(?<=:)\s([\u4e00-\u9fa5a-zA-Z0-9]{1,7})[^\n]{1,35}(|\n)` + + +> 以下是使用自定义的目录规则,无法设置左右间距 + +![Image](https://github.com/user-attachments/assets/38d5f327-eaad-4105-aae7-52cf32cdcb94) + +> 以下是使用自带的目录规则,可以正常设置左右间距 + +![Image](https://github.com/user-attachments/assets/ee94fbb3-382c-443d-a454-834225c84d55) + +## #663 [web/server]:阅读了几章后,点击顶部首页按钮返回书架页后,阅读进度没有刷新,还是显示之前阅读进度,点击进入后也会回到之前的阅读进度 + +原始链接:https://github.com/hectorqin/reader/issues/663 +创建:2025/5/28 23:24:28;关闭:2026/9/23 9:17:29;原因:not_planned;历史评论总数:4。时间为 UTC。 + +**为避免无效问题和冗余问题,提问前请确认** +1. 你确定Google不能解决你的问题 +2. 你确定已有的issue不能解决你的问题 +3. 你确定issue的title按照格式如下:[web/simple-web/server]:description + +**Describe the bug 描述你遇到的问题** +A clear and concise description of what the bug is. 简洁有效的说明。 + +阅读小说中点击首页按钮,书籍进度不会刷新 + +**To Reproduce 如何重现问题** +Steps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容: +1. Go to 小说阅读界面,阅读几章之后 +2. Click on '首页' +3. 可以看到书架上刚刚阅读的书,进度并没有更新,点进去也会回到之前的章节。除非点首页返回后,再次下拉刷新后,书架上的进度才会更新 + +**Expected behavior 期待修复的效果** +A clear and concise description of what you expected to happen. 简单描述。 + +点首页返回后,能正确刷新进度 + +**Screenshots 如有必要,可以截图说明** +If applicable, add screenshots to help explain your problem. + +**版本说明** + - OS: [e.g. win] unraid + - Deploy Method docker compose + - Program Version v3.2.14-05250935 + - Browser [e.g. chrome, safari] win edge, android edge + +**Additional context 其他说明** +Add any other context about the problem here. 添加你认为有必要的内容,否则不写。 + + +## #658 提供 t2s 等新版开源阅读js命令 + +原始链接:https://github.com/hectorqin/reader/issues/658 +创建:2025/5/13 11:07:33;关闭:2026/9/23 9:17:28;原因:not_planned;历史评论总数:0。时间为 UTC。 + +如题 + +## #657 原先导入的小说,突然在页面中不显示了 + +原始链接:https://github.com/hectorqin/reader/issues/657 +创建:2025/5/5 3:59:23;关闭:2026/9/23 9:17:28;原因:not_planned;历史评论总数:1。时间为 UTC。 + +Docker部署的,书源能正常显示,但是小说却不显示了。并且我去容器内看了,小说数据其实都还是在的,只是不知道为什么页面不显示了【难道小说的展示与否并不是根据存储目录中是否有这本小说,而是有一个专门的[配置文件]记录每次导入的新小说,然后页面展示小说就是根据这个[配置文件]来展示?然后莫名其妙这个[配置文件]消失了,所以原先导入的小说都不展示了?然后我重新导入了一本新的,于是这个[配置文件]重建了,所以才会只显示导入的这一本小说?】 + +![Image](https://github.com/user-attachments/assets/bf81474e-2796-443d-b027-ce2e36ed2828) + +![Image](https://github.com/user-attachments/assets/ea4e4964-b177-491c-b515-e4c0573b6d8c) + +## #656 [server]macOS下的safari导出书籍时,文件名乱码 + +原始链接:https://github.com/hectorqin/reader/issues/656 +创建:2025/4/18 15:13:33;关闭:2026/9/23 9:17:27;原因:not_planned;历史评论总数:0。时间为 UTC。 + +如题,文件名中的中文变成URL编码。 + +**To Reproduce 如何重现问题** +只要导出书籍必定会出现。 + +**版本说明** + - OS: [e.g. win] macOS15.4.1 + - Deploy Method 说明软件部署方式 群晖Docker + +**Additional context 其他说明** +AI说是部分服务器未遵循标准,在返回文件时未正确设置 Content-Disposition 头部的 filename* 参数(用于 UTF-8 编码),导致 Safari 无法自动解码文件名。Safari 对非 ASCII 字符(如中文、日文、特殊符号)的处理较严格,若服务器未明确指定 UTF-8 编码,Safari 会直接保留 URL 编码格式,而非尝试解码。 + + +## #654 目录规则:添加自定义规则以及设置默认规则 + +原始链接:https://github.com/hectorqin/reader/issues/654 +创建:2025/4/15 4:26:39;关闭:2026/9/23 9:17:27;原因:not_planned;历史评论总数:1。时间为 UTC。 + +关于目录规则的两个建议: +1、添加自定义规则(目前只有内置的一些规则,添加完书籍后,然后在阅读界面可重选以及修改规则,但是下次再添加书籍时要用相同规则需要重新加一遍) +2、设置某个规则为默认规则(目前是内置规则) +![Image](https://github.com/user-attachments/assets/15385785-340f-4db1-8647-613582d5b346) + +## #655 服务器版本老是跳,有时候跳段落,有时候跳章节 + +原始链接:https://github.com/hectorqin/reader/issues/655 +创建:2025/4/15 15:48:40;关闭:2026/9/23 9:17:27;原因:not_planned;历史评论总数:3。时间为 UTC。 + +![Image](https://github.com/user-attachments/assets/1d72e022-360f-41f3-bad0-09c24a9ac282) + +这样设置有问题吗 + +## #651 调试源结果正常,但是在阅读UI界面添加书籍,反应找不到书,怎么回事? + +原始链接:https://github.com/hectorqin/reader/issues/651 +创建:2025/4/12 4:33:06;关闭:2026/9/23 9:17:26;原因:not_planned;历史评论总数:0。时间为 UTC。 + +阅读docker版本 v3.2.12-12171550。系统ubuntu 24.04.2。浏览器firefox,版本137.0。 +写了一个书源,书源内容见以下。 +``` +{ + "bookSourceUrl": "https://www.xiguasuwu.com/", + "bookSourceType": 0, + "bookSourceName": "西瓜书屋", + "bookSourceGroup": "自编", + "bookSourceComment": "", + "loginUrl": "", + "loginUi": "", + "loginCheckJs": "", + "concurrentRate": "", + "header": " {\n'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36 Edg/90.0.818.56',\n'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',\n'Accept-Encoding': 'gzip, deflate, br',\n'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6'\n}", + "bookUrlPattern": "", + "searchUrl": "https://www.xiguasuwu.com/search,{'method':'POST','body':'searchkey={{key}}'}", + "exploreUrl": "", + "enabled": true, + "enabledExplore": true, + "weight": 0, + "customOrder": 0, + "lastUpdateTime": 1744422386179, + "ruleSearch": { + "bookList": "class.panel-body@class.book-coverlist", + "name": "class.fs-16@tag.a@text", + "author": "class.fs-14@text", + "intro": "class.fs-12@text", + "bookUrl": "class.fs-16@tag.a@href" + }, + "ruleExplore": {}, + "ruleBookInfo": { + "name": "class.bookTitle@text", + "author": "class.glyphicon-user@text", + "kind": "class.breadcrumb@tag.li1@text", + "wordCount": "class.glyphicon-font@text", + "lastChapter": "class.text-danger@text", + "intro": "id.bookIntro@text", + "coverUrl": "class.img-thumbnail@data-original", + "tocUrl": "class.btn-success@href" + }, + "ruleToc": { + "chapterList": "id.newlist@tag.dd", + "chapterName": "class.col-sm-4@tag.a@text", + "chapterUrl": "class.col-sm-4@tag.a@href", + "nextTocUrl": "class.right@tag.a@href!class.before" + }, + "ruleContent": { + "content": "id.booktxt@text", + "nextContentUrl": "id.linkNext@href" + } +} +``` + +调试源,可以得到正文页解析完成。调试回报见以下附件。 +[阅读调试log.txt](https://github.com/user-attachments/files/19717654/log.txt) +然后将书源保存,确认阅读UI界面里有该书源。然后只用该源分类,搜索书籍,反馈找不到书。书名为“绿龙筑巢记”,即是调试源时用的搜索书名。不知道是什么原因,有朋友知道什么原因,或者怎么检查? +[reader-2025-04-12.log](https://github.com/user-attachments/files/19716378/reader-2025-04-12.log) +阅读的日志文件,隐去个人网址。 + +## #652 移动端浏览器进度异常跳跃问题【尝试解决中】 + +原始链接:https://github.com/hectorqin/reader/issues/652 +创建:2025/4/12 5:29:04;关闭:2026/9/23 9:17:26;原因:not_planned;历史评论总数:16。时间为 UTC。 + +1、屏幕触控、滑动经常乱跳乱翻页 +2、浏览器界面不能全屏,建议去komga漫画应用学习一下 + +## #650 请求支持startBrowserAwait去除人机验证 + +原始链接:https://github.com/hectorqin/reader/issues/650 +创建:2025/4/10 12:03:07;关闭:2026/9/23 9:17:25;原因:not_planned;历史评论总数:1。时间为 UTC。 + +现在大部分的网站都启用了人机验证,大部分的书源通过以下方案去除人机验证: + +if(result.match(/Just a moment/)){ +cookie.removeCookie(source.bookSourceUrl) +var x=baseUrl; +var s=java.startBrowserAwait(x,"验证").body() +result=s; + }else{ + result=result; + } +result + + +reader3服务器版目前还不支持,使用时时会报错 +Error: org.mozilla.javascript.EcmaError: TypeError: Cannot find function startBrowserAwait in object io.legado.app.model.analyzeRule.AnalyzeRule@1baa25b8 +请问大佬是否可以支持,谢谢 + +## #649 求助:目录能否自动刷新 + +原始链接:https://github.com/hectorqin/reader/issues/649 +创建:2025/4/10 7:44:39;关闭:2026/9/23 9:17:25;原因:not_planned;历史评论总数:1。时间为 UTC。 + +docker部署的阅读服务端。 +使用本地txt作为书籍来源,作为来源的本地txt内容会自动更新。 +但本地更新后在阅读web界面书本不会自动更新内容,需要在书本详情里点击目录-刷新,才能更新章节及内容。 +想请问大佬有没有解决方法或者是能够调用的章节刷新接口,谢谢! + +## #647 大佬求助:字符集错误导致文本无法正常显示 + +原始链接:https://github.com/hectorqin/reader/issues/647 +创建:2025/4/9 5:17:35;关闭:2026/9/23 9:16:52;原因:not_planned;历史评论总数:1。时间为 UTC。 + +**为避免无效问题和冗余问题,提问前请确认** +1. 你确定Google不能解决你的问题 +2. 你确定已有的issue不能解决你的问题 +3. 你确定issue的title按照格式如下:[web/simple-web/server]:description + +**Describe the bug 描述你遇到的问题** +书源可以缓存,但由于字符集不匹配无法正常显示 +可以正常缓存,获取封面,获取目录 +在reader安卓原版客户端上使用正常 + +**To Reproduce 如何重现问题** +Steps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容: +1. 添加“速读谷”书源 https://www.yckceo.com/yuedu/shuyuan/json/id/5862.json +2. 随便添加一本书到书架,我添加的是“夜无疆” +3. 打开书籍 +4. 随便选中间的一个章节加载(这里很奇怪,第一次用这个书源的时候前两个章节都可以正常加载,后面突然不行了) +5. 页面显示java字符集报错 + +![Image](https://github.com/user-attachments/assets/efa13e74-c9cb-44e4-8099-fc8cb3f540d5) + +对应log +``` +[f26ccad3] [DefaultDispatcher-worker-2] 12:50:59.249 ERROR YueduApi - onHandlerError: +java.nio.charset.IllegalCharsetNameException: initial-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width + at java.nio.charset.Charset.checkName(Charset.java:315) + at java.nio.charset.Charset.lookup2(Charset.java:484) + at java.nio.charset.Charset.lookup(Charset.java:464) + at java.nio.charset.Charset.forName(Charset.java:528) + at io.legado.app.help.http.OkHttpUtilsKt.text(OkHttpUtils.kt:117) + at io.legado.app.help.http.OkHttpUtilsKt.text$default(OkHttpUtils.kt:102) + at io.legado.app.help.http.OkHttpUtilsKt.newCallStrResponse(OkHttpUtils.kt:78) + at io.legado.app.help.http.OkHttpUtilsKt$newCallStrResponse$1.invokeSuspend(OkHttpUtils.kt) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) + at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106) + at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571) + at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750) + at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678) + at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665) +``` + +**Expected behavior 期待修复的效果** +可以正常显示章节文本 + +**Screenshots 如有必要,可以截图说明** + + +**版本说明** + - OS: [e.g. win] Linux + - Deploy Method Docker + - Program Version 3.2.13 + - Browser [e.g. chrome, safari] Chrome + +**Additional context 其他说明** +Add any other context about the problem here. 添加你认为有必要的内容,否则不写。 + + +## #645 [server]翻页模式bug + +原始链接:https://github.com/hectorqin/reader/issues/645 +创建:2025/4/7 16:48:19;关闭:2026/9/23 9:16:52;原因:not_planned;历史评论总数:3。时间为 UTC。 + +“上下滑动”翻页模式在章末会有大片的空白。 + +版本说明 + + - OS: [iOS] + - Deploy Method docker + - Program Version v3.2.13 + - Browser [safari] + +## #644 docker版目录 + +原始链接:https://github.com/hectorqin/reader/issues/644 +创建:2025/4/7 5:21:03;关闭:2026/9/23 9:16:51;原因:not_planned;历史评论总数:4。时间为 UTC。 + +大佬你好,请问能否重置或指定nas里的目录,而不是root里面,就是扫描nas本地目录,从而添加书籍 + +## #642 每次升级版本就会把之前上传的小说重置 + +原始链接:https://github.com/hectorqin/reader/issues/642 +创建:2025/4/6 1:39:55;关闭:2026/9/23 9:16:51;原因:not_planned;历史评论总数:1。时间为 UTC。 + +我用docker安装的,每次升级版本就会把之前上传的小说重置 + +![Image](https://github.com/user-attachments/assets/91522c5e-2f84-4542-8fd5-a24374d109fd) + +## #643 过滤规则:是否能够将文本替换为换行 + +原始链接:https://github.com/hectorqin/reader/issues/643 +创建:2025/4/6 5:27:27;关闭:2026/9/23 9:16:51;原因:not_planned;历史评论总数:0。时间为 UTC。 + +我要将两个空格替换为换行,我在过滤规则中使用了/n , /r, /r/n,启动或是关闭“使用正则表达式”,都不成功,都是直接将两个空格替换为了/n , /r, /r/n的文本,请问过滤规则能够实现将文本替换为换行吗 + +## #638 web/server 书架过大的时候会出现超时 + +原始链接:https://github.com/hectorqin/reader/issues/638 +创建:2025/3/20 12:30:45;关闭:2026/9/23 9:16:50;原因:not_planned;历史评论总数:1。时间为 UTC。 + +**Describe the bug 描述你遇到的问题** +无法加载书架数据,导致无法查看书籍 + +2025-03-20 20:14:11 [09201f70] [DefaultDispatcher-worker-4] INFO c.h.r.api.controller.BaseController - Loop: 3681 concurrentCount: 16 lastIndex: 58910 endIndex: 58917 costTime: 37853 ms deferredList size: 0 +2025-03-20 20:14:11 [09201f70] [DefaultDispatcher-worker-4] INFO c.h.r.api.controller.BaseController - Loop: 3682 concurrentCount: 6 lastIndex: 58916 endIndex: 58917 costTime: 37863 ms deferredList size: 0 +2025-03-20 20:14:12 [09201f70] [DefaultDispatcher-worker-4] INFO i.v.e.w.h.impl.LoggerHandlerImpl - 10.0.3.1 - - [Thu, 20 Mar 2025 12:13:32 GMT] "GET /reader3/getBookshelf?refresh=0&v=1742472812088 HTTP/1.1" 200 40385249 "https://公网IP/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36" +2025-03-20 20:14:12 [1e01fe14] [vert.x-eventloop-thread-0] INFO c.h.reader.verticle.RestVerticle - 客户端已断开链接,终止运行 + +![Image](https://github.com/user-attachments/assets/ceb65b7c-21d0-48a0-81e8-36327dff856b) +超时后,后端设定会报红 + +![Image](https://github.com/user-attachments/assets/e1df17c3-76bb-4427-99a9-5d729379c8c8) +我的书架文件bookshelf大概有60M + +**To Reproduce 如何重现问题** +Steps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容: +进入页面开始加载书架就会出现,完全无法加载书籍 + +**Expected behavior 期待修复的效果** +希望书架信息可以在项目启动时预加载到缓存中,更新数据的时候,对缓存也进行更新删除操作 + +**版本说明** + - OS: [e.g. win] 说明操作系统 + - linux + - Deploy Method 说明软件部署方式 + - docker + - Program Version 说明软件版本 + - 3.2.12 + - Browser [e.g. chrome, safari] 说明终端、浏览器型号 + - chrome + +**Additional context 其他说明** +Add any other context about the problem here. 添加你认为有必要的内容,否则不写。 + + +## #641 [server] 关于自动同步配置造成的问题 + +原始链接:https://github.com/hectorqin/reader/issues/641 +创建:2025/4/5 16:16:26;关闭:2026/9/23 9:16:50;原因:not_planned;历史评论总数:0。时间为 UTC。 + +在同时打开“自动同步”和配置方案“自动切换”时,自动切换日/夜模式会失效。 + +表现为在冷启动时,先获取了系统的深色模式,再触发了自动同步,同步后把在系统获取到的深色模式覆盖掉了。 + + +**版本说明** + - OS: [iOS] + - Deploy Method docker + - Program Version v3.2.13 + - Browser [safari] + +## #637 每过一段时间,都会自动删除我使用的账号 + +原始链接:https://github.com/hectorqin/reader/issues/637 +创建:2025/3/15 9:08:22;关闭:2026/9/23 9:16:49;原因:not_planned;历史评论总数:6。时间为 UTC。 + +**Describe the bug 描述你遇到的问题** +1.每过一段时间没登录,都会自动删除我使用的账号,登录提示账号未注册 +2.重新注册同名账号后,所有数据都在,仅仅是账号莫名其妙消失了 +3.大概每过1个月左右?如果没登录的话,账号就不见了,并且我并未设置多少天未登录自动删除账号这个环境变量 + +**To Reproduce 如何重现问题** +1.重现过两次,每次都是长时间未登录,然后账号消失,但账号的数据却又都在 + +**Expected behavior 期待修复的效果** +1.希望能找到账号被删除的原因,毕竟我并未设置“**天未登录就自动删除”的环境变量,还是说docker在未设置环境变量的情况下,本身就默认自带“30天未登录就删除账号”这个环境变量?我用了几年了,记得以前不会的,不知道从去年哪个版本开始就开始会删账号了 + +**版本说明** +UNRAID系统下,docker部署 + + + + +## #636 阅读epub时,iframe和解析模式的bug + +原始链接:https://github.com/hectorqin/reader/issues/636 +创建:2025/3/12 4:43:30;关闭:2026/9/23 9:16:49;原因:not_planned;历史评论总数:0。时间为 UTC。 + + +**Describe the bug 描述你遇到的问题** +阅读epub时,iframe和解析模式的bug + +**To Reproduce 如何重现问题** +1. 将epub解析设置成iframe模式 +2. 显示为这样(标题和图片重叠,字体样式有问题) +![Image](https://github.com/user-attachments/assets/f684880c-f806-4b9a-9c65-d622a1224447) +3.shitf+f5忽略缓存刷新后,将epub解析设置为解析模式 +4.显示正常 +![Image](https://github.com/user-attachments/assets/9b06e98c-fccb-4c12-ad88-5d037d8c963c) +5.点击下一章或者刷新页面后,格式错乱(之后无论怎么刷新或者切换解析模式都是错乱的) +![Image](https://github.com/user-attachments/assets/d7db2f09-558a-4244-938c-7cfb00b95726) +6.将epub解析再次设置为iframe模式,shitf+f5忽略缓存刷新,之后再将epub解析设置为解析模式。显示正常 +但是点击下一章或者刷新页面后还是格式错乱 +结论:只有将epub解析设置成iframe模式后shitf+f5忽略缓存刷新,再设置成解析模式,才能出现一次正常显示的页面,之后无论点击下一章还是刷新页面都是错乱的,需要重复此步骤才能再次正常显示 + + +**Expected behavior 期待修复的效果** +设置成解析模式时无论点击下一章还是刷新页面都正常显示 + +**版本说明** + - OS: LINUX + - Deploy Method: docker + - Program Version : v3.2.12 + - Browser : chrome浏览器(https访问) + +**Additional context 其他说明** +epub文件(下载后解压): +[epub文件.zip](https://github.com/user-attachments/files/19202858/epub.zip) + +## #635 在Arc浏览器使用reader的朗读功能,跳到下一章的时候,总是从文章的中间开始。 + +原始链接:https://github.com/hectorqin/reader/issues/635 +创建:2025/3/10 2:07:11;关闭:2026/9/23 9:16:48;原因:not_planned;历史评论总数:3。时间为 UTC。 + +在Arc浏览器使用reader的朗读功能,跳到下一章的时候,总是从文章的中间开始,~~但是使用chrome并没有这个问题~~(听了一上午的chrome,也发现存在这个问题),可以随意复现。 + +**版本说明** + - macos Sonoma 14.0 + - docker本地部署 + - 软件版本:V3.2.12-12171550 + - arc版本:版本1.85.0 (59596) + - 内核:Chromium Engine Version 134.0.6998.45 + + +## #632 当使用mongo数据库时,依然会丢失书源 + +原始链接:https://github.com/hectorqin/reader/issues/632 +创建:2025/3/5 7:05:09;关闭:2026/9/23 9:16:48;原因:not_planned;历史评论总数:1。时间为 UTC。 + +**为避免无效问题和冗余问题,提问前请确认** +1. 你确定Google不能解决你的问题 +2. 你确定已有的issue不能解决你的问题 +3. 你确定issue的title按照格式如下:[web/simple-web/server]:description + +**Describe the bug 描述你遇到的问题** +A clear and concise description of what the bug is. 简洁有效的说明。 + +**To Reproduce 如何重现问题** +Steps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior 期待修复的效果** +A clear and concise description of what you expected to happen. 简单描述。 + +**Screenshots 如有必要,可以截图说明** +If applicable, add screenshots to help explain your problem. + +**版本说明** + - OS: [e.g. win] 说明操作系统 + - Deploy Method 说明软件部署方式 + - Program Version 说明软件版本 + - Browser [e.g. chrome, safari] 说明终端、浏览器型号 + +**Additional context 其他说明** +Add any other context about the problem here. 添加你认为有必要的内容,否则不写。 + + +## #634 [web/simple]:kindle 的simple-waeb翻页闪屏 + +原始链接:https://github.com/hectorqin/reader/issues/634 +创建:2025/3/7 16:21:34;关闭:2026/9/23 9:16:48;原因:not_planned;历史评论总数:1。时间为 UTC。 + +kindle在使用simple-web版的网页翻页时会出现闪屏(全屏刷新)问题,但使用正常版网页,并不会出现闪屏。在正常版的网页使用时,将动画时长设定为0,动画速度设定为1000,无闪屏,且使用体验还可以。但是正常版的内存占用太大,使用卡顿。能否增加一个类似的翻页效果,来解决全屏刷新的问题。 + + +**版本说明** + - OS: kindle + - Deploy Method kindle 浏览器 + - Program Version 3.2.12 + - Browser kindle withe paper 5 ,内置浏览器 + +**Additional context 其他说明** +kpw 5有水波纹刷新,也许增加一个类似的动画。 + +## #630 从书仓导入PDF文件,大部分页面变成空白 + +原始链接:https://github.com/hectorqin/reader/issues/630 +创建:2025/3/1 3:32:22;关闭:2026/9/23 9:16:47;原因:not_planned;历史评论总数:0。时间为 UTC。 + +**为避免无效问题和冗余问题,提问前请确认** +1. 你确定Google不能解决你的问题 +2. 你确定已有的issue不能解决你的问题 +3. 你确定issue的title按照格式如下:[web/simple-web/server]:description + +**Describe the bug 描述你遇到的问题** +从书仓导入PDF文件,大部分页面变成空白。 + +**To Reproduce 如何重现问题** +Steps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior 期待修复的效果** +A clear and concise description of what you expected to happen. 简单描述。 + +**Screenshots 如有必要,可以截图说明** +If applicable, add screenshots to help explain your problem. + +**版本说明** + - OS: [e.g. win] MacOS 15.3.1 + - Deploy Method 群晖docker + - Program Version v3.2.12-12171550 + - Browser [e.g. chrome, safari] safari + +Image + +Image + +Image + +**Additional context 其他说明** +Add any other context about the problem here. 添加你认为有必要的内容,否则不写。 + + +## #626 希望把java端整合在setup + +原始链接:https://github.com/hectorqin/reader/issues/626 +创建:2025/2/8 8:42:02;关闭:2026/9/23 9:16:47;原因:not_planned;历史评论总数:0。时间为 UTC。 + +希望把java端整合在setup,不是每个人都需要java,只是为了方便使用reader,而单独去安装一个java,没必要。这里指win端 ,web端还好,可以用apt,yum等直接安装,而且作者给的123盘的java链接已经失效 + +## #624 翻页。。。在平板上不行啊,上下页不衔接 + +原始链接:https://github.com/hectorqin/reader/issues/624 +创建:2025/1/28 2:31:18;关闭:2026/9/23 9:16:46;原因:not_planned;历史评论总数:2。时间为 UTC。 + +翻页。。。在平板上不行啊,上下页不衔接 + +## #622 可以隐藏任务栏吗 + +原始链接:https://github.com/hectorqin/reader/issues/622 +创建:2025/1/13 1:29:58;关闭:2026/9/23 9:16:46;原因:not_planned;历史评论总数:0。时间为 UTC。 + +**为避免无效问题和冗余问题,提问前请确认** +1. 你确定Google不能解决你的问题 +2. 你确定已有的issue不能解决你的问题 +3. 你确定issue的title按照格式如下:[web/simple-web/server]:description + +**Describe the bug 描述你遇到的问题** +A clear and concise description of what the bug is. 简洁有效的说明。 + +**To Reproduce 如何重现问题** +Steps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior 期待修复的效果** +A clear and concise description of what you expected to happen. 简单描述。 + +**Screenshots 如有必要,可以截图说明** +If applicable, add screenshots to help explain your problem. + +**版本说明** + - OS: [e.g. win] 说明操作系统 + - Deploy Method 说明软件部署方式 + - Program Version 说明软件版本 + - Browser [e.g. chrome, safari] 说明终端、浏览器型号 + +**Additional context 其他说明** +Add any other context about the problem here. 添加你认为有必要的内容,否则不写。 + + +## #623 [web]:IOS设备使用safari等webkit内核浏览器在最新版本系统中无法使用大声朗读\httpTTS的tts + +原始链接:https://github.com/hectorqin/reader/issues/623 +创建:2025/1/18 15:57:29;关闭:2026/9/23 9:16:46;原因:not_planned;历史评论总数:4。时间为 UTC。 + +**Describe the bug 描述你遇到的问题** +IOS设备使用safari等webkit内核浏览器在最新版本系统中无法使用大声朗读\HTTPTTS的tts + +**To Reproduce 如何重现问题** +Steps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容: +1. 打开任意文字内容 +2. 打开TTS版面 +3. 朗读方式选取大声朗读或者httpTTS(已经配置好AZURE TTS) +4. 点击开始按钮,按钮没有变成暂停键,同时文字TTS部分被标红,就没有任何反应了 + +**Screenshots 如有必要,可以截图说明** + +![Image](https://github.com/user-attachments/assets/7ae699e4-4387-4804-bd85-13bbd39f9391) + +![Image](https://github.com/user-attachments/assets/b33fbf53-fb13-4927-82b8-e3920335d696) + +**版本说明** + - OS:iPhone/iPad iOS18.2 + - Deploy Method :Docker In DSM7.2.2 + - Program Version :3.2.12 + - Browser :iOS浏览器:safari,firefox,chrome,edge + +**Additional context 其他说明** +经过穷举法,目前测试出只有夸克可以正常播放(见下图),理论上应该也是Webkit内核才对,原因不明 +其他所有设备都能正常播放。 + +![Image](https://github.com/user-attachments/assets/8e3a8172-d011-4dd3-acef-1d7c0e479d18) + +## #618 删除书籍后,assets目录下的export与cover未删除 + +原始链接:https://github.com/hectorqin/reader/issues/618 +创建:2024/12/17 0:30:16;关闭:2026/9/23 9:16:45;原因:not_planned;历史评论总数:0。时间为 UTC。 + +点击导入书籍,上传txt格式的书,设置了封面,然后导出epub,之后删除这本书,发现`assets/用户/export `里面曾经导出过的epub文件还残留, `assets/用户/cover`里面曾经上传的封面图片还在,书多了以后,这些残留资源该如何清理? + +## #616 大声朗读局域网IP可以使用,使用花生壳域名访问无法使用 DMZ主机是开着的 + +原始链接:https://github.com/hectorqin/reader/issues/616 +创建:2024/12/16 7:19:44;关闭:2026/9/23 9:16:45;原因:not_planned;历史评论总数:8。时间为 UTC。 + +能不能让大声朗读不要用梯子也可以使用 + +## #613 分享一个自定义Docker TTS 语音项目 可配 web TTS 朗读 + +原始链接:https://github.com/hectorqin/reader/issues/613 +创建:2024/12/7 15:44:54;关闭:2026/9/23 9:16:44;原因:not_planned;历史评论总数:15。时间为 UTC。 + +项目 https://github.com/zuoban/tts?tab=readme-ov-file + +使用方法 +1、自行配置上述Docker +2、打开本项目reader的web页面,随便点开一部小说,然后点朗读图标 +3、点击HttpTTS添加,弹出的默认示例中名字可以自己定义,其他不需要输入,url输入这个 +`http://192.168.1.X:8080/tts?t={{java.encodeURI(speakText)}}&v=zh-CN-YunxiNeural&r={{java.encodeURI(speakSpeed)}}*4&p=0&o=audio-24khz-48kbitrate-mono-mp3` + +``` +参数列表: +1. t: 文本内容 (必填) +2. v: 语音名称 (可选), 默认为 zh-CN-XiaoxiaoMultilingualNeural +3. r: 语速 (可选), 默认为 0 +4. p: 语调 (可选), 默认为 0 +5. o: 输出格式 (可选), 默认为audio-24khz-48kbitrate-mono-mp3 +``` + + +## #614 数据库+redis方案 + +原始链接:https://github.com/hectorqin/reader/issues/614 +创建:2024/12/11 2:44:22;关闭:2026/9/23 9:16:44;原因:not_planned;历史评论总数:1。时间为 UTC。 + +**Describe the bug 描述你遇到的问题** +自部署版本在导入书源、校验书源等步骤时运行卡顿,且消耗内存过大 + +**To Reproduce 如何重现问题** +Steps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容: +1. 导入书源 +2. 选择本地书源文件导入(json格式) + +或 + +1. 失效书源 +2. 检测书源 + +**Expected behavior 期待修复的效果** +1. 在检测书源是否失效页面增加 `停止` 、`取消` 按钮,点击取消能取消全部操作,点击停止能停止校验(已校验过的结果保留) +![image](https://github.com/user-attachments/assets/9c3e0c70-2ca9-4e51-b042-184aa1601ce8) + +2. 新增连接外置数据库方法及说明,如MySQL、Postgres、Mongo、Sqllite等 + +3. 新增连接外置redis加快缓存速度 + +**版本说明** + - OS: 群晖Synology + - Deploy Method :Docker + - Program Version :3.2.11 + - Browser :Chrome + +**Additional context 其他说明** +1. 建议隐藏 `后端设定` 选项,改错了前端容易崩溃 +![image](https://github.com/user-attachments/assets/aa2afbb4-d0da-43a6-a6e4-f229ed749047) +2. `WdbDav` 增加 `坚果云` 等第三方远程连接方式,以便和手机端通过第三方同步 +3. `其它` 改为 `关于` 并排版到最下面(与Github链接合并) +![image](https://github.com/user-attachments/assets/5697a718-8ba0-4239-941b-7980caf21c6d) + + +## #608 服务器版手机 edge 浏览器自带大声朗读无法播放 + +原始链接:https://github.com/hectorqin/reader/issues/608 +创建:2024/11/11 13:21:53;关闭:2026/9/23 9:16:43;原因:not_planned;历史评论总数:1。时间为 UTC。 + +服务器版手机端用 edge 浏览器自带大声朗读无法播放,不是项目自带的阅读功能,电脑上面没问题![Screenshot_2024-11-11-21-13-32-293_com.microsoft.emmx.jpg](https://github.com/user-attachments/assets/647576fc-05b2-408a-8ac2-ef560e3857dc) + +用 legado 的 app web 服务来链接到 reader3 上就没问题![IMG_20241111_213707.jpg](https://github.com/user-attachments/assets/072a5414-fd4e-453e-9a84-50a081404130) + +![Screenshot_2024-11-11-21-34-24-932_com.microsoft.emmx.jpg](https://github.com/user-attachments/assets/52d881e5-fdfc-48bb-8374-9c35df9914a6) + + +还有目录加载速度很慢 + +## #601 添加本地书籍报错 获取章节内容失败! 章节文件不存在 + +原始链接:https://github.com/hectorqin/reader/issues/601 +创建:2024/10/30 20:45:25;关闭:2026/9/23 9:16:43;原因:not_planned;历史评论总数:3。时间为 UTC。 + + +![image](https://github.com/user-attachments/assets/f9aa7db0-553d-4c83-9c26-c6fde4eba7fe) + + +## #596 页面显示的章节与打开书籍后显示的章节不一致 + +原始链接:https://github.com/hectorqin/reader/issues/596 +创建:2024/10/22 11:50:07;关闭:2026/9/23 9:16:43;原因:not_planned;历史评论总数:0。时间为 UTC。 + +1.问题描述 +页面显示的章节与打开书籍后显示的章节不一致 + +2.问题发生的位置 +安卓APP和本项目网页端 + +3.问题的详细描述 +手机端阅读APP设置webdav同步,安卓APP和网页端均能正常同步章节。但是在点击进入书籍后,打开的章节与显示的不一致。 + +4.问题复现 +手机端阅读APP阅读至543章,点开网页发现能正常同步进度 +1 + +但是点开书籍后显示的章节却是536章 +2 +注:手机端安卓APP和网页端均存在该问题。 + + + + +**版本说明** +安卓阅读APP 3.23.031210 +本项目的版本 v3.2.10-07270648 +浏览器 火狐、Safari +部署方式docker + + + + +## #594 【建议】希望编辑后可以永久生效,而不是刷新后就会失效 + +原始链接:https://github.com/hectorqin/reader/issues/594 +创建:2024/10/13 4:20:05;关闭:2026/9/23 9:16:42;原因:not_planned;历史评论总数:0。时间为 UTC。 + +![1728792866759](https://github.com/user-attachments/assets/ed88e1dc-cef0-4b0f-99e3-47d95f02fac6) +![1728792885992](https://github.com/user-attachments/assets/3c7c4e71-8687-49a3-bc17-df3920183667) +因为小说内容可能会有错别字或者乱码之类的,我看到了就想手动改掉或者删除,但是现在这个编辑功能就有点鸡肋,没什么实际效果【感觉只是改了HTML,变更的内容并没有真的保存起来】,刷新页面又出来了 + +## #590 请教下各位书友httptts 怎么配置 + +原始链接:https://github.com/hectorqin/reader/issues/590 +创建:2024/9/24 6:30:13;关闭:2026/9/23 9:15:23;原因:not_planned;历史评论总数:1。时间为 UTC。 + +(无正文) + +## #585 是否能修改下翻页的一个问题,章尾最后一页的排版,最后一行字始终是在页面最底部 + +原始链接:https://github.com/hectorqin/reader/issues/585 +创建:2024/8/30 13:01:21;关闭:2026/9/23 9:15:22;原因:not_planned;历史评论总数:0。时间为 UTC。 + +当阅读到章尾时,最后一页的最后一行字始终是在页面最底部,而不是依据上一页最后内容来排版 +这样每次到章尾时,眼睛都要上下扫扫,才能找刚才看到哪了 + +图一为 上一页最后内容 +![1](https://github.com/user-attachments/assets/7fd97a48-ee0b-42b9-ab42-55aa21d7927a) + +图二为 最后一页排版 +![2](https://github.com/user-attachments/assets/be94b45a-3862-4c20-a923-e288599d46eb) + +图三 希望能把上一页已阅读的内容隐藏了 +![3](https://github.com/user-attachments/assets/05f27315-f1ef-4c91-99a3-e3f58e11f1ed) + + +## #588 windows桌面版和安卓app版怎么和服务端连接 + +原始链接:https://github.com/hectorqin/reader/issues/588 +创建:2024/9/8 16:49:23;关闭:2026/9/23 9:15:22;原因:not_planned;历史评论总数:6。时间为 UTC。 + +windows桌面版和安卓app版怎么和服务端连接呢,想要app或者桌面上看服务端上本地传了书 + +## #584 建议增加标题的替换功能 + +原始链接:https://github.com/hectorqin/reader/issues/584 +创建:2024/8/29 2:30:02;关闭:2026/9/23 9:15:21;原因:not_planned;历史评论总数:0。时间为 UTC。 + +建议增加标题的替换功能,方便正则去除求月票、求订阅、XX加更之类的内容 + +## #580 v3.2.10版本问题汇总 + +原始链接:https://github.com/hectorqin/reader/issues/580 +创建:2024/8/15 13:43:35;关闭:2026/9/23 9:15:21;原因:not_planned;历史评论总数:5。时间为 UTC。 + +1.键盘->下一章出现跳章问题,目前最大跳四章; +2.朗读时候最小化状态切换下一章不是从章头开始朗读(历史版本一直存在此问题); + +## #573 [web]:切换书源界面功能强化-章节关键字搜索以及显示字数 + +原始链接:https://github.com/hectorqin/reader/issues/573 +创建:2024/8/2 9:28:42;关闭:2026/9/23 9:15:21;原因:not_planned;历史评论总数:2。时间为 UTC。 + + +**Describe the bug 描述你遇到的问题** +希望可以象Legado 阅读一样,在切换书源时,可以直接搜索关键字(这样可以搜出已经更新出最新章节的源)并显示章节字数(这样可以知道哪些源是真正更新了该章内容的,那种只有几十个字百来个字的显然不合格) + +以下是Legado阅读和reader相关界面的对比: +![tempImageDPoIDx](https://github.com/user-attachments/assets/6e145a83-0d4e-4d24-890f-1c8ab41ba55a) +![Longshot-20240802172024](https://github.com/user-attachments/assets/4c7289b9-e007-4b27-b8b0-418d1e3436b5) + +**版本说明** + - OS: [e.g. win] MacOS最新 + - Deploy Method: Docker + - Program Version:最新 + - Browser [e.g. chrome, safari] MacBook Pro, Safari + + + +## #539 希望优化翻页方式(滚动) + +原始链接:https://github.com/hectorqin/reader/issues/539 +创建:2024/6/4 10:26:30;关闭:2026/9/23 9:15:20;原因:not_planned;历史评论总数:4。时间为 UTC。 + + +翻页方式-滚动翻页,全屏点击-不翻页后。手机触摸滚动屏幕时候偶尔还是会触发整页翻动。希望优化成手机触摸滚动屏幕不要触发页面跳转。 + +## #536 希望优化翻页 + +原始链接:https://github.com/hectorqin/reader/issues/536 +创建:2024/6/1 8:30:38;关闭:2026/9/23 9:15:20;原因:not_planned;历史评论总数:4。时间为 UTC。 + +翻页功能能否自定义,不要一点击屏幕就自动跳转翻页。手机使用经常不小心点击到就跳转了其他内容 + +## #523 Kindle的浏览器打不开 + +原始链接:https://github.com/hectorqin/reader/issues/523 +创建:2024/5/8 6:01:02;关闭:2026/9/23 9:15:19;原因:not_planned;历史评论总数:5。时间为 UTC。 + +如题 +然后在reader里点7天试用,邮箱填了收不到验证码邮件 + +## #529 [web/server]: “上下滚动”的翻页方式会导致切换书的时候页面空白 + +原始链接:https://github.com/hectorqin/reader/issues/529 +创建:2024/5/26 20:13:27;关闭:2026/9/23 9:15:19;原因:not_planned;历史评论总数:3。时间为 UTC。 + +使用 “上下滚动”的翻页方式切换书的时候,必然出现三种情况中一种: +1. 点进去的时候当前章节内容部分会变成空白,需要手动在章节列表点击具体章节,或用快捷键切换到下一章才能正常显示。 +2. 点进去的时候当前章节内容部分先是依然是上本书的章节内容,卡顿几秒后跳转到新书的某个章节,该章节可能是上上次阅读到的位置,或别的设备阅读到的位置,而不是书架中显示的最新的阅读进度(书架中显示的最新的阅读进度是正确的)。随后书架中显示的最新的阅读进度也变成了这个随机章节。 +3. 当前章节内容部分能正常显示,但是章节列表变成空白,也无法用快捷键切换到下一章,需要手动刷新章节列表。 + +书均为来自本地书仓的txt文件。 + +当选择“上下滑动”翻页方式的时候,则没有任何问题。 + +**版本说明** + - OS: 群晖 + - Deploy Method: Docker + - Program Version: 3.2.8 + - Browser: Safari, macOS + + +## #519 使用nginx反向代理的redaer自带webdav作为阅读3.0的备份服务器,在阅读3.0导入远程书籍时显示超时 + +原始链接:https://github.com/hectorqin/reader/issues/519 +创建:2024/4/30 16:23:36;关闭:2026/9/23 9:15:19;原因:not_planned;历史评论总数:5。时间为 UTC。 + +### 确认 / Assignments + +- [X] 搜索现有issues,不存在相似或相关的issue / No similar or related issues +- [X] 最新[测试版](https://github.com/gedoor/legado/actions/workflows/test.yml)依然存在此问题 / Latest beta app does not work +- [X] 此问题和Xposed、Lsposed、Magisk、手机主题、浏览器插件等无关 / Make sure your machine is not touched by hook frameworks, plugins etc + +### 问题描述 / Describe Bugs + +自己使用docker搭建的reader服务器,使用其自带的webdav作为书库。经过nginx反向代理为子目录访问,访问链接为https://test.com:1122/dav/。可以正常备份和恢复,本地书籍或缓存好的书籍可以正常导入至webdav。在远程书籍中也能够看到目录下的书籍,但是从远程书籍导入时失败,日志显示导入出错,timeout。查看nginx日志,access.log中只有一条记录"PROPFIND /dav/reader3/webdav/legado/books HTTP/1.1" 207 22862 "-" "Chrome/119.0.0.0 Safari/537.36 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)"。error.log中无任何记录。当代理为location /{}时情况一样。 + + +我的nginx设置为: +server { + #添加加ssl和http2协议的要求。 + listen 11loca22 ssl ; #ipv4 + #修改为自己申请的域名 + server_name test.com; + ###添加秘钥 + # Mozilla Intermediate configuration + ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; + #以下协议需要确认硬件是否支持,如果不支持或者不确定可以直接去除 + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + #以下修改为自己申请的域名公钥和私钥文件地址 + ssl_certificate ; + ssl_certificate_key ; + ############## + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + if ($server_port !~ 1122){ + rewrite ^(/.*)$ https://$host$1 permanent; + } + error_page 497 https://$host$request_uri; + +location / { + +} + + location /dav/ { +##### + gzip on; #开启gzip压缩 + gzip_min_length 1k; #设置对数据启用压缩的最少字节数 + gzip_buffers 4 16k; + gzip_http_version 1.0; + gzip_comp_level 6; #设置数据的压缩等级,等级为1-9,压缩比从小到大 + gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml; #设置需要压缩的数据格式 + gzip_vary on; + client_max_body_size 50m; #允许上传50MB文件,上传本地书籍需要修改此项大小.如nginx主配置文件已添加,删除此行并修改主配置即可 +#修改为自己需要代理的局域网ip和端口号 + proxy_pass http://112.128.1.21:1122/; #阅读 + proxy_http_version 1.1; + proxy_cache_bypass $http_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + } + + access_log /var/log/nginx/access.log; #; + error_log /var/log/nginx/error.log; # ; +} + + + + + +### 复现步骤 / How to reproduce + +每次使用都是如此 + +### 确认 / Assignment + +- [X] 已经提交复现所需要的附加资料 / Submit additions related with bugs + +### 其他信息 / Additions + +_No response_ + +### 日志提交 / Relevant log output + +_No response_ + +### 阅读版本 / Legado version + +3.23.110211 + +### Android版本 / Android version + +HarmonyOS 4.0.0 + +### 机型 / Model + +HUAWEI Mate 40 + +## #513 书源失效时服务器缓存不起作用 + +原始链接:https://github.com/hectorqin/reader/issues/513 +创建:2024/4/5 15:38:10;关闭:2026/9/23 9:15:18;原因:not_planned;历史评论总数:1。时间为 UTC。 + +发现书源失效的时候之前浏览过的小说就看不了了, +即使在内容已经缓存在服务器的情况下 +![屏幕截图 2024-04-05 233558](https://github.com/hectorqin/reader/assets/31752207/5a2037ae-b60d-4ba6-b44e-92988278e2a0) +在缓存文件夹有内容的情况下,无法设置启用缓存数据代替书源数据 + +## #512 竖排书籍无法正常翻页 + +原始链接:https://github.com/hectorqin/reader/issues/512 +创建:2024/4/3 14:01:34;关闭:2026/9/23 9:15:18;原因:not_planned;历史评论总数:1。时间为 UTC。 + +竖排书籍无法正常翻页,只能左右滑动,导致无法存储位置。 +点击翻页区域,直接跳到上一章或者下一章。 + +以下为提供的竖排epub书籍: + +[越女剑.epub](https://alist.toyqiu.cc:999/d/nas/eBooks/CalibreWeb/Jin%20Yong/Yue%20Nu%20Jian%20(1005)/Yue%20Nu%20Jian%20-%20Jin%20Yong.epub?sign=UPpc4qt_BHHbDRf7MrN1fxDYQQL9EpN5JuR4Ycirf_I=:0) + +[かばん屋の相続.epub](https://alist.toyqiu.cc:999/d/nas/eBooks/CalibreWeb/Chi%20Jing%20Hu%20Run/kabanWu%20noXiang%20Sok%20(1189)/kabanWu%20noXiang%20Sok%20-%20Chi%20Jing%20Hu%20Run.epub?sign=0G2nvkOdPz-yNCGUNCrXiwBo3ZMcrRzv5S4zrlf7IVM=:0) + +## #498 书籍目录无法获取完整 + +原始链接:https://github.com/hectorqin/reader/issues/498 +创建:2024/2/29 12:35:23;关闭:2026/9/23 9:15:17;原因:not_planned;历史评论总数:11。时间为 UTC。 + +**为避免无效问题和冗余问题,提问前请确认** +1. 你确定Google不能解决你的问题 是 +2. 你确定已有的issue不能解决你的问题 是 + +**Describe the bug 描述你遇到的问题** + +- **部分epub书籍的目录在sigil,calibre-web中可以正常识别目录结构,但是这里并不能完整提取** + +![Snipaste_2024-02-29_20-31-20](https://github.com/hectorqin/reader/assets/52244378/452d28f7-5b87-4f74-8377-a18b1ac4e53e) +![Snipaste_2024-02-29_20-26-53](https://github.com/hectorqin/reader/assets/52244378/b3c5e92d-ae48-402a-a98a-f57199a0c346) +![Snipaste_2024-02-29_20-25-27](https://github.com/hectorqin/reader/assets/52244378/c3974a5f-6e97-404f-b036-972b80c7bcc2) + +**版本说明** + - OS: [e.g. win] 说明操作系统:ubuntu 22.04 + - Deploy Method 说明软件部署方式: docker compose + - Program Version 说明软件版本:v3.2.7-01010833 + + +## #491 请问怎么设置单次搜索次数?现在需要一直点击加载更多 + +原始链接:https://github.com/hectorqin/reader/issues/491 +创建:2024/2/6 10:39:25;关闭:2026/9/23 9:15:17;原因:not_planned;历史评论总数:2。时间为 UTC。 + +找了下文档和docker的yaml没看到描述此类的配置项,有些书书源多点方便换源,感觉服务器这块,直接检索完全部书源也不是不可以,是否有这个功能呢,有的话在哪里配置呢 + +## #486 [server]:有充足内存的情况下报错OutOfMemoryError + +原始链接:https://github.com/hectorqin/reader/issues/486 +创建:2024/1/24 13:09:41;关闭:2026/9/23 9:15:16;原因:not_planned;历史评论总数:4。时间为 UTC。 + +**Describe the bug 描述你遇到的问题** +有充足内存的情况下报错OutOfMemoryError ,类似于 #429 ,但明确并非真的发生了内存不足 + +**To Reproduce 如何重现问题** +Steps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容: +1. 使用docker运行[reader](https://github.com/hectorqin/reader),并限制内存为2G +2. 对内存占用进行监测 +3. 使用10并发进行书源检测 +4. 大量请求无返回,达到超时请求后失败;查看日志发现大量报错OutOfMemoryError,但此时仍有充足内存供使用。 +log如:Exception in thread "DefaultDispatcher-worker-xx" java.lang.OutOfMemoryError: Java heap space + +**Expected behavior 期待修复的效果** +在用完或接近用完时再报错OutOfMemoryError,方案为增加自定义堆内存大小,如允许在JVM启动时指定参数且不会被脚本覆盖 +`-Xmx2048m`,如在下面这行就会和手动设置的内存数冲突。 + +https://github.com/hectorqin/reader/blob/b3f580f49385376b88f92818b5a9dfa874e1f80f/server/bin/startup.sh#L96 + +**Screenshots 如有必要,可以截图说明** +容器内存情况 +![image](https://github.com/hectorqin/reader/assets/44089074/9adbb302-5bc5-4672-9cc1-9bbb1e748acb) +宿主机内存情况 +![image](https://github.com/hectorqin/reader/assets/44089074/be0427bf-20d3-4878-b7ac-e3bf8dde8a6b) +日志 +![image](https://github.com/hectorqin/reader/assets/44089074/b2df2c30-e64a-4035-8612-544bfb71f11b) + + +**版本说明** + - OS: Ubuntu 22.04 + - Deploy Method: Docker + - Program Version: `v3.2.7-01010833` + - Browser: Google Chrome v120 + + +## #484 书籍列表分页的问题 + +原始链接:https://github.com/hectorqin/reader/issues/484 +创建:2024/1/18 3:25:31;关闭:2026/9/23 9:15:16;原因:not_planned;历史评论总数:5。时间为 UTC。 + +我采用了reader的docker last版本,然后导入了本地9000多本书籍 +当我进入书籍页面后发现,加载书籍列表需要非常长的时间(测试估计在30秒左右) +然后我看了一下请求,发现 getbookshelf接口的确没有进行分页,而是将所有的书籍返回 + +是否有计划下一步进行更新? + +![image](https://github.com/hectorqin/reader/assets/8120401/27901d59-1577-413f-8015-9d80ee78844d) + +![image](https://github.com/hectorqin/reader/assets/8120401/0cbb4687-629f-445d-8c74-9cde7a41fb58) + + +## #482 IOS, web端, 使用翻页的时候, 下一章的内容会跳转到最后,而不是最前,且语音听书的时候经常会有卡住的情况 + +原始链接:https://github.com/hectorqin/reader/issues/482 +创建:2024/1/17 2:01:59;关闭:2026/9/23 9:15:15;原因:not_planned;历史评论总数:2。时间为 UTC。 + +IOS, web端。 +1、使用翻页的时候, 偶尔会出现下一章的内容会跳转到最后,而不是最前。 +2、语音听书的时候经常会有卡住的情况,需要重新刷新页面才行。 +3、web端上下滑动、上下滚动、翻页的时候会存在翻多页的情况,体验不加。 +4、IOS使用网页浏览,内置的主题切换会导致页面展示出现问题,比如白天我看到第七页换成夜晚就变成了第2页这种。 + +## #475 RSS Feed Bug + +原始链接:https://github.com/hectorqin/reader/issues/475 +创建:2024/1/3 7:47:17;关闭:2026/9/23 9:15:14;原因:not_planned;历史评论总数:1。时间为 UTC。 + +RSS Feed报错: +org.xmlpull.v1.XmlPullParserException: No valid parser classes found in parameter classNames to newInstance() that contained ' org.kxml2.io.KXmlParser org.kxml2.io.KXmlSerializer ' + + +## #463 本地书籍整理功能支持 + +原始链接:https://github.com/hectorqin/reader/issues/463 +创建:2023/11/25 2:46:01;关闭:2026/9/23 9:15:14;原因:not_planned;历史评论总数:1。时间为 UTC。 + +你好,我有1.8G本地书需要整理 +Q1:如果在文件夹里多个txt分别为第一章第二章第三章,必须手动合并到一个txt才能识别?能否能增加功能支持 + +Q2:本地一个分组就有上百本书籍,能否增加本地搜索支持 + +Q3:本地txt添加不同分组导入后,发现他存到了reader\storage\data\default文件夹,但是我本地有1.8G的TXT,也按照分类分好文件夹了,能否出个调用本地文件夹的功能,这样数据就不用再存到reader文件夹下,如果必须存,能否将default文件夹内加上分组文件夹,这样我就把本地txt删了 + +## #454 可不可以加入背景透明化 + +原始链接:https://github.com/hectorqin/reader/issues/454 +创建:2023/11/6 6:53:57;关闭:2026/9/23 9:15:14;原因:not_planned;历史评论总数:1。时间为 UTC。 + +类似于这位开发的背景虚化功能 +https://github.com/binbyu/Reader/blob/master/README.md + +## #453 发一下windows Java安装教程 + +原始链接:https://github.com/hectorqin/reader/issues/453 +创建:2023/11/6 1:12:22;关闭:2026/9/23 9:15:13;原因:not_planned;历史评论总数:0。时间为 UTC。 + +我是小白,之前没接触过需要安装Java的项目,因为安装成功,分享我的步骤。 + +1、下载 Java 8 安装包。可以从 Oracle 官网 https://www.java.com/download/ie_manual.jsp 下载。 + +2、安装 Java 8。安装过程非常简单,只需要按照屏幕上的提示操作即可。 + +3、配置 Java 环境变量。在 Windows 系统中,可以通过以下步骤来配置 Java 环境变量: + +打开控制面板,然后单击“系统”。 +在“系统”窗口中,单击“高级系统设置”。 +在“高级”选项卡中,单击“环境变量”。 +在“系统变量”列表中,单击“新建”。 +在“变量名”框中键入“JAVA_HOME”,在“变量值”框中键入 Java 安装目录的路径。例如,如果 Java 安装目录为 C:\Program Files\Java\jdk1.8.0_271,则变量值为 C:\Program Files\Java\jdk1.8.0_271。 +单击“确定”。 +在“系统变量”列表中,单击“Path”。 +在“变量值”框的末尾添加 %JAVA_HOME%\bin。 +单击“确定”。 +验证 Java 环境是否安装成功。在命令提示符中输入以下命令: + +java -version +如果显示 Java 版本信息,则表示 Java 环境安装成功。 + +需要特别注意的是,在Reader设置,Java路径应当具体到exe文件。列如X:\Java\bin\java.exe + +## #353 WebDAV的疑问 + +原始链接:https://github.com/hectorqin/reader/issues/353 +创建:2023/4/13 14:20:44;关闭:2026/9/23 9:14:41;原因:completed;历史评论总数:27。时间为 UTC。 + +1、阅读APP上WebDAV设置里有个子文件夹,修改这个子文件夹我APP端点备份后会在这个文件夹里生成一个带设备名称的压缩包。而且阅读服务器端点WebDAV保存备份是生成在legado内不带设备名称的压缩包。那服务器端和阅读APP的书籍进度不是存在一个目录内的吗?服务器端好像没有可用修改子文件夹的地方。 +2、阅读APP上有个远程书籍,目录是WebDAV内的books文件夹。我在阅读服务器端上把文件上传到这个目录然后加入书架,但阅读APP上充远程书籍上放入书籍一直提示导入出错这是为什么?有什么方法可以把本地书籍放在WebDAV上然后服务器和APP端都能同步进度的吗? +其次必须阅读APP子文件夹目录修改成默认的legado书架才能同步吗? + +## #339 为用jb的IDE的程序员们开发了个reader的阅读插件 + +原始链接:https://github.com/hectorqin/reader/issues/339 +创建:2023/3/22 15:39:56;关闭:2026/9/23 9:14:25;原因:completed;历史评论总数:5。时间为 UTC。 + +## 在IDE中随时随地阅读,提升编码效率。 +### 同时支持`开源阅读app`和`服务器版阅读` + +都是为了我们程序员能够随时阅读书籍,提升自我 +### 点击链接开始阅读:https://github.com/nancheung/legado-reader + +![image](https://user-images.githubusercontent.com/34061813/226959469-82b4485b-ed3c-4db8-95d1-6f215712abbf.png) + +![image](https://user-images.githubusercontent.com/34061813/226958375-7b44e1e3-52ec-48be-ba8e-a3e89b2e27e9.png) + + +## #720 书源功能建议 + +原始链接:https://github.com/hectorqin/reader/issues/720 +创建:2026/9/22 11:36:32;关闭:2026/9/23 7:14:13;原因:completed;历史评论总数:1。时间为 UTC。 + +之前一直使用旧版本reader,在安装了新版本reader 后,书源的使用遇到困难。 +希望出个书源导入的教程。 + +另外,按照README 来安装,需要修改一下,默认命令无法拉取镜像: +docker run -d --name reader \ + -p 8080:8080 \ + -v /path/to/your/books:/books:ro \ + -v ./data:/data \ + cnb.cool/hectorqin/reader:**main** + +### 近期评论:hectorqin,2026/9/23 7:14:13 +https://github.com/hectorqin/reader/issues/720#issuecomment-5790657565 + +目前还在开发中,未发布镜像,而且目前只聚焦本地书库,是不支持legado书源的。 + +## #719 新版本安装问题及书源导入方法 + +原始链接:https://github.com/hectorqin/reader/issues/719 +创建:2026/9/22 7:34:42;关闭:2026/9/22 8:26:51;原因:completed;历史评论总数:2。时间为 UTC。 + +使用更新后的readme来安装,docker安装时有个问题:拉取不到镜像。使用如下的方式可以拉取 +docker run -d --name reader \ + -p 8080:8080 \ + -v /path/to/your/books:/books:ro \ + -v ./data:/data \ + cnb.cool/hectorqin/reader:_main_ + +另外有个疑问或者说建议: +老版本的书源导入方式,复制链接后就可以自动同步。 +新版本还不知道怎么导入书源。 + +### 近期评论:eking929,2026/9/22 7:35:24 +https://github.com/hectorqin/reader/issues/719#issuecomment-5772817643 + +谢谢你的来信,看到后我会尽快回复! + +### 近期评论:hectorqin,2026/9/22 8:26:51 +https://github.com/hectorqin/reader/issues/719#issuecomment-5773422708 + +新版本目前聚焦本地书库,暂时不考虑支持远程书源相关功能 diff --git a/docs/closed-issues-2026-09-23/recent-comments.json b/docs/closed-issues-2026-09-23/recent-comments.json new file mode 100644 index 0000000..f77439d --- /dev/null +++ b/docs/closed-issues-2026-09-23/recent-comments.json @@ -0,0 +1,26 @@ +[ + { + "issue_url": "https://api.github.com/repos/hectorqin/reader/issues/719", + "html_url": "https://github.com/hectorqin/reader/issues/719#issuecomment-5772817643", + "created_at": "2026-09-22T07:35:24Z", + "updated_at": "2026-09-22T07:35:24Z", + "author": "eking929", + "body": "谢谢你的来信,看到后我会尽快回复!" + }, + { + "issue_url": "https://api.github.com/repos/hectorqin/reader/issues/719", + "html_url": "https://github.com/hectorqin/reader/issues/719#issuecomment-5773422708", + "created_at": "2026-09-22T08:26:51Z", + "updated_at": "2026-09-22T08:26:51Z", + "author": "hectorqin", + "body": "新版本目前聚焦本地书库,暂时不考虑支持远程书源相关功能" + }, + { + "issue_url": "https://api.github.com/repos/hectorqin/reader/issues/720", + "html_url": "https://github.com/hectorqin/reader/issues/720#issuecomment-5790657565", + "created_at": "2026-09-23T07:14:13Z", + "updated_at": "2026-09-23T07:14:13Z", + "author": "hectorqin", + "body": "目前还在开发中,未发布镜像,而且目前只聚焦本地书库,是不支持legado书源的。" + } +] diff --git a/docs/closed-issues-analysis-2026-09-23.md b/docs/closed-issues-analysis-2026-09-23.md new file mode 100644 index 0000000..f563452 --- /dev/null +++ b/docs/closed-issues-analysis-2026-09-23.md @@ -0,0 +1,179 @@ +# 最近两天关闭 Issue 的需求方向分析 + +分析日期:2026-09-23(北京时间)。对象:hectorqin/reader 的关闭 issue,以及当前本地工作区代码。 + +## 结论 + +建议以“可靠的个人书库阅读器”为主线:优先保证进度、阅读和数据不会出错,再补齐书内检索、批注、多级目录、缓存管理等日常能力。远程来源继续沿用插件架构,避免把旧版 Legado 的全部兼容性负担重新放回核心。 + +建议顺序:**发布与接入说明 → 阅读/进度可靠性与数据恢复 → 书内搜索、批注和目录 → 朗读与离线体验 → 书源质量与生态互通。** + +下文优先级是根据影响范围、当前基础和产品定位给出的建议,不是维护者已承诺的排期。 + +## 1. 抓取范围与解释 + +- GitHub REST Search:`repo:hectorqin/reader is:issue is:closed closed:>=2026-09-21`,分页抓取 101 条,再核对关闭时间。 +- 以 2026-09-23 17:31 北京时间为分析截止,最近 48 小时内共 101 条;按北京时间 9 月 22—23 日统计结果相同。9 月 22 日 1 条,9 月 23 日 100 条。 +- `state_reason`:97 条 `not_planned`,4 条 `completed`(#339、#353、#719、#720)。旧问题在数分钟内密集关闭,与批量清理历史 backlog 的特征一致;不能解释为两天新增的需求,更不能解释为全部已修复。 +- 完整抓取全部标题、正文、创建/关闭时间、标签和评论数量;另抓取 9 月 21 日起更新的仓库 issue 评论,相关评论为 #719 的 2 条和 #720 的 1 条。**没有抓取全部历史评论,也没有逐一检查图片和附件。** +- 当前能力通过 README、使用手册、架构文档及关键源码交叉核对。没有启动应用执行兼容性测试,因此“已有实现”不等于“已验证旧问题全部消失”。 +- 完整正文和索引见 [issue 存档](closed-issues-2026-09-23/issues.md),结构化数据见 [issues.json](closed-issues-2026-09-23/issues.json)。 + +### 发布口径与工作区能力存在差异 + +[维护者在 #719 的回复](https://github.com/hectorqin/reader/issues/719#issuecomment-5773422708)表示“新版本目前聚焦本地书库,暂时不考虑支持远程书源相关功能”;[在 #720 的回复](https://github.com/hectorqin/reader/issues/720#issuecomment-5790657565)表示“目前还在开发中,未发布镜像……不支持 legado 书源”。 + +本地工作区已包含 local、OPDS、通用进程插件、远程章节、换源和追更。这说明本地开发能力与对外口径需要同步,但不能据此推断公开镜像已经提供这些功能。通用插件能力也不等于原生支持 Legado JSON 书源。 + +## 2. 需求方向与当前差距 + +### A. 安装、迁移和来源接入:P0 + +**依据:** [#719](https://github.com/hectorqin/reader/issues/719)、[#720](https://github.com/hectorqin/reader/issues/720)、#714、#666、#588。 + +**用户目标:** 部署成功后知道如何读到第一本书,旧用户知道哪些数据和书源可以继续使用。 + +**当前基础:** 已有 Docker 入门文档、插件管理 UI、npm/.tgz 安装和来源实例管理。入门文档仍展示 `cnb.cool/hectorqin/reader:latest`,但上述回复称尚未发布镜像;本次未核验镜像仓库标签。 + +**建议需求:** + +1. 明确稳定版、开发版、旧版的入口、镜像标签、能力范围和数据目录,发布前核验文档命令可用。 +2. 首次使用提供本地目录、OPDS、插件三种接入路径,并显示是否配置成功。 +3. Legado JSON/订阅链接导入时给出明确兼容性说明;若由独立插件支持,提供具体插件及安装步骤,不能只提示“添加书源”。 +4. 旧版迁移先明确账号、书架、进度、书源的可迁移范围,再设计预检查和迁移摘要;不能把旧 `/storage` 直接当新 `/data`。 + +**验收:** 新用户按发布文档部署并读到一本书;旧用户能在导入前知道兼容范围;不兼容输入显示可操作的解释。 + +### B. 阅读与进度可靠性:P0 + +**依据:** #702、#698、#696、#692、#687、#663、#655、[#652](https://github.com/hectorqin/reader/issues/652)、#645、#624、#596、#585、#580、#539、#536、#529、#512、#482。 + +**用户目标:** 不跳章、不漏段,换设备或重新打开仍在刚才的位置。 + +**当前基础:** 已有分页/滚动、手势和定位器、跨窗口加载、离线 outbox、按账号同步、按设备保存显示设置。它们是完善和回归的基础,不应重做一套阅读器。 + +**建议需求:** + +1. 将“打开位置正确、翻页连续、退出前保存”作为版本验收门槛,覆盖 iOS Safari、Android WebView 和桌面浏览器。 +2. 明确滚动到章末、点击翻页、键盘和音量键的跨章行为;手势一次只触发一个动作。 +3. 字号、主题、旋转屏幕、工具栏显隐后保留正文锚点,不能靠旧页码恢复位置。 +4. 显示同步状态和失败重试入口;提供进度历史/恢复能力,处理多设备离线后回传旧位置的情况。 + +**验收:** 连续读过章节边界后退出重进,正文位置与书架进度一致;A/B 两设备交替阅读及断网重连不发生无提示回退;切换排版不丢当前段落。 + +### C. 数据保护与大书库:P0/P1 + +**依据:** [#674](https://github.com/hectorqin/reader/issues/674)、#657、#642、#637、#632、#638、[#484](https://github.com/hectorqin/reader/issues/484)、#463、#644、#704、#697、#707、#618、#614。 + +**当前基础:** SQLite、独立 DATA_DIR、直接扫描 BOOKS_DIR、稳定内容身份、元数据覆盖、分页和目录递归筛选已有实现。书架服务默认每页 50、最多 200;不能再把“新增分页”作为空白功能立项。现有备份文档要求停机后复制完整数据目录。 + +**建议需求:** + +1. 提供备份/恢复的产品入口或可靠管理命令,包含账号、进度、笔记、插件配置及一致性校验;恢复前可预览。 +2. 把“文件暂时离线”“索引失效”“账号书架隐藏”“数据损坏”区分显示,提供重扫或恢复路径。 +3. 验证 1 万本及更大书库下首屏、查询、增量扫描和同步性能;前端也要避免一次性载入全部数据。 +4. 增加存储用量、封面/章节/下载缓存分类、日志轮转部署示例与可预览清理。 +5. 本地 TXT 追加更新应考虑版本关系和进度继承。当前内容哈希参与身份,文件内容变化可能成为新副本;“自动重扫”并不自动保证追更连续性。 + +**验收:** 升级、重启和移动文件不丢阅读记录;备份恢复后账号和进度可用;缓存清理不删原书;大书库结果按页展示。 + +### D. 书内搜索、批注与内容整理:P1 + +**依据:** [#692](https://github.com/hectorqin/reader/issues/692)、[#709](https://github.com/hectorqin/reader/issues/709)、#710、#498、#654、#659、#688、#594、#643、#584。 + +**当前差距:** + +- 书架/书库的书名、作者搜索已存在;没有找到完整的书内全文搜索入口和命中段落定位流程。 +- 笔记、高亮、书签的数据类型、API 和离线同步已存在;没有找到阅读 UI 中创建、编辑、列表和定位的完整操作链。因此应归为“补齐产品交互”,不宜直接写成已完成。 +- Web EPUB nav 解析将条目设为 `depth: 0`,章节匹配去掉 fragment;NCX 使用正则提取嵌套结构。服务端也主要提取章节标题。真实多级目录和章内锚点需要专项补齐。 +- 元数据编辑已持久化,但不代表正文纠错、标题替换或 TXT 目录规则已支持。 + +**建议需求:** + +1. 书内全文检索:结果摘要、命中高亮、跳到具体段落,保留返回原阅读位置的入口。 +2. 选区高亮 → 添加批注 → 笔记列表 → 点击回正文,复用现有同步与墓碑删除机制。 +3. 正确保留 EPUB nav/NCX 层级和 fragment,支持折叠、多级定位;TXT 增加可复用的章节规则及预览。 +4. 正文纠错和过滤规则先采用可撤销的覆盖层,保存于 DATA_DIR;刷新不丢,规则更新可回退。只有用户明确选择且书库可写时才考虑写回原文件。 + +**验收:** 同一章多个搜索命中定位不同段落;批注跨设备恢复;三级目录不丢节点;刷新后纠错仍在且可撤销。 + +### E. 朗读体验与配置诊断:P1 + +**依据:** #690、[#689](https://github.com/hectorqin/reader/issues/689)、#635、#623、#616、#613、#608、#590、#580、#482。 + +**当前基础:** 已有浏览器、Android 原生及 HTTP 三类引擎,语音选择、逐句高亮、自动续章、服务端代理及 TTS 缓存上限。 + +**建议需求:** 增加配置检测与试听;区分无语音、鉴权失败、上游超时和浏览器播放限制;验证 HTTPS/反代/iOS 下播放、后台返回和续章定位;失败可重试或切换引擎。 + +**验收:** 管理员可用一次试听确认配置;续章从章首开始;失败有具体原因,不返回空成功;停止后没有残留音频继续播放。 + +### F. 离线缓存与书源质量:P1/P2 + +**依据:** [#513](https://github.com/hectorqin/reader/issues/513)、#717、#718、#716、#573、#672、#706、#703、#650、#658、#647、#491、#697。 + +**当前基础:** 已读章节优先使用服务端缓存,来源失效前已缓存的内容有继续读取的基础;搜索可流式展示和取消;已有同一来源实例内的换源和个人凭据接口。凭据接口不等于支持任意网站的交互登录,通用插件不等于兼容全部 Legado JS。 + +**建议需求:** + +1. P1:整书/章节范围预下载、暂停/继续/取消、离线可读状态、按用户/书籍的配额和清理。明确“服务端缓存”与“设备断网可读”两个层次。 +2. P2:换源候选显示当前章字数、最新章节、响应时间和获取错误;先解决明显空章,再考虑相似度,避免把相似度当内容正确性的证明。 +3. P2:插件能力兼容表、登录状态/过期提示与必要的人工验证流程;解析和网站适配留在插件侧。 +4. EPUB 导出和 NAS 保存作为独立能力评估,需要版本、保存位置、失败重试及存储边界。它与 OPDS 浏览是不同需求。 + +**验收:** 预下载后断网可读指定范围;来源停用不损坏已有书架和缓存;任务可取消且配额可见;换源失败仍保留原内容和进度。 + +### G. 生态互通和客户端覆盖:P2 + +**依据:** [#679](https://github.com/hectorqin/reader/issues/679)、#673、#717、#700、#353、#519、#693、#588、#712、#523、#634。 + +**当前基础:** 已有 OPDS 客户端获取能力和 Web/Android;当前检查范围内没有找到 OPDS 服务端目录发布和 Legado WebDAV 同步路由。 + +**建议:** + +- 连接 Calibre 等 OPDS 书库归为已有功能的易用性完善。 +- “把 reader 书库供其他客户端使用”需要 OPDS 服务端能力,另行立项,不能与 OPDS 客户端混为一谈。 +- WebDAV 文件备份、账号数据备份、第三方阅读进度互通是三种问题。若做 Legado 兼容,单独定义书籍身份与定位映射适配器。 +- 先明确支持的浏览器版本和电纸书低动画方案;独立 iOS/Windows ARM 客户端按需求规模另评估。 + +### H. 可选增强:P3 + +**依据:** #694 AI 章节总结、#699 外部评论、#711 自定义标题、#454 背景透明化、#475 RSS。 + +AI 速读适合在书内定位、内容访问和插件边界稳定后评估,需包含章节来源定位、调用成本和用户配置。网页标题隐私选项成本可能较低,可穿插实现。外部评论和 RSS 会扩展产品范围,建议暂缓。 + +旧 Java 安装、JVM 内存参数、Jackson CVE 等(#453、#626、#486、#670、#695)不应直接转为当前 Node 服务端的对应修改;应保留其“易部署、受限资源运行、依赖维护”的目标。#339、#613 属于生态分享,不直接按缺陷计数。 + +## 3. 当前已覆盖、应转为验收的能力 + +| 历史诉求 | 当前证据 | 后续动作 | +| --- | --- | --- | +| 本地目录挂载、减少复制、子目录搜索 | `ShelfService.list`、扫描器、文件管理 | 做数据规模与权限场景验收 | +| 大书架分页 | `server/src/services/shelf.ts` 的 page/pageSize | 检查前端、同步和首屏是否仍全量加载 | +| OPDS 接入 | `server/src/sources/opds.ts` | 完善添加与鉴权体验;勿宣称已有 OPDS 服务端 | +| 不同设备独立主题 | `web/src/store/settings.ts` 按设备持久化 | 验证账号同步不覆盖显示设置 | +| 自动追更和换源 | `chapter-updates.ts`、`source-host.ts` | 验证插件能力声明、换源落点与失败回滚 | +| 失效源已有章节可读 | `ChapterPublications.asset` 在取源前返回已存正文 | 验证目录与设备缓存;补整书预下载 | +| TTS 引擎与续章 | `web/src/render/speech.ts`、`server/src/services/tts.ts` | 补配置诊断与平台验收 | +| 笔记/高亮同步 | `SyncService`、`OfflineStore` 和 notes API | 补阅读器完整交互,不作为已完成产品功能 | + +## 4. 建议下一批需求拆分 + +| 顺序 | 交付切片 | 可评审成果 | +| --- | --- | --- | +| 1 | 新旧版与来源接入说明 | 已核验部署命令、发布能力表、首次接入流程 | +| 2 | 进度可靠性 | 跨设备/离线/重排场景验收、同步状态、进度恢复方案 | +| 3 | 数据恢复 | 一致性备份、恢复校验、升级与目录变化验证 | +| 4 | 书内检索和目录 | 段落命中定位、三级目录、fragment 保留 | +| 5 | 笔记操作闭环 | 选区批注、笔记列表、回跳和跨设备同步 | +| 6 | 离线及听书 | 下载任务、缓存管理、TTS 试听和异常提示 | +| 7 | 插件生态增强 | 登录能力声明、换源质量、兼容矩阵和互通方案 | + +优先级不按关闭时间或关闭原因机械排序;这批数据混有多年旧版本反馈、使用问题和生态分享。建议在立项时对各切片补充新版本真实用户验证,而非直接复刻旧版功能集合。 + +## 5. 源码与文档核对入口 + +- [使用手册](user-guide.zh-CN.md)、[架构](architecture.md)、[来源插件](source-plugins.md)、[插件扩展](plugin-extensions.md)。 +- [书架分页与递归筛选](../server/src/services/shelf.ts)、[同步服务](../server/src/services/sync.ts)、[客户端同步](../web/src/core/sync.ts)。 +- [设备设置](../web/src/store/settings.ts)、[阅读器交互](../web/src/ui/reader-screen.tsx)、[阅读工具栏](../web/src/ui/reader-chrome.tsx)。 +- [Web EPUB 目录解析](../web/src/formats/epub.ts)、[服务端 EPUB 解析](../server/src/indexer/formats/epub.ts)。 +- [章节缓存](../server/src/publications/chapters.ts)、[来源 API](../server/src/http/routes/sources.ts)、[TTS 服务](../server/src/services/tts.ts)。 diff --git a/docs/development.zh-CN.md b/docs/development.zh-CN.md index 07c74dc..8de4cba 100644 --- a/docs/development.zh-CN.md +++ b/docs/development.zh-CN.md @@ -81,7 +81,7 @@ UI 改动可运行 `npm run ui:review --prefix web`,书源流程可运行 `npm docker build -f server/Dockerfile -t reader:local . ``` -构建上下文必须是仓库根目录,镜像会同时构建 Web 和服务端。部署时将 Compose 的镜像名改为 `reader:local`,或使用文件内的 `build` 配置。 +构建上下文必须是仓库根目录,镜像会同时构建 Web 和服务端。可执行 `docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build` 使用源码部署;后续管理也保留两个 `-f` 参数。 ### 不使用容器 diff --git a/docs/ecosystem-roadmap.zh-CN.md b/docs/ecosystem-roadmap.zh-CN.md new file mode 100644 index 0000000..e4f53d2 --- /dev/null +++ b/docs/ecosystem-roadmap.zh-CN.md @@ -0,0 +1,37 @@ +# P2 生态互通与客户端覆盖方案 + +日期:2026-09-24。依据:[关闭 issue 分析](closed-issues-analysis-2026-09-23.md) F、G。 + +## 当前可使用的能力 + +- Reader 的 OPDS **客户端**可连接 Calibre 等外部目录,浏览、搜索并获取文件;具体搜索、分页、下载和鉴权取决于目录服务。 +- 来源页可查看插件声明的能力。个人凭据仅显示是否配置,以及当前账号最近一次访问的结果;不代表网站会话持续有效。 +- 章节换源可按所选章节检测正文、查看最新章节和耗时。字符数和可读性不能证明章节完整、内容正确或站点授权。 +- 阅读设置提供电纸书预设:浅色、分页、无翻页动画。设备自行管理刷新率、残影与全刷;这不是原生电纸书客户端。 + +## 独立交付项目 + +| 项目 | 首版建议边界 | 关键决策及验收 | +| --- | --- | --- | +| OPDS 服务端 | 只读发布当前账号有权访问的文件书籍;分页、搜索、封面和原文件下载 | 使用可撤销的客户端凭据;不向外泄露文件路径;两个账号互不可见;停用凭据后不能继续获取资源;用 Calibre/至少一个移动客户端验收。章节书暂不发布,待有可下载出版物再接入。 | +| WebDAV 文件备份 | 上传已完成并校验的一致性备份包,复用 P0 备份/恢复流程 | 不直接同步运行中的 SQLite;临时上传后完成发布;断网重试、配额、保留策略、凭据加密;下载后校验并在独立目录恢复。与书籍文件保存任务分离。 | +| Legado 进度互通 | 独立适配器,明确对方版本、备份格式、书籍身份及定位映射 | 先只读导入并预览冲突;书名作者相同不能直接判为同一本;来源变化、缺章、多设备冲突、时间差均有明确策略;无法映射的记录必须可解释且不覆盖本地。 | +| 章节 EPUB 导出 | 将用户有权获取的指定章节快照导出为一个任务产物 | 固定目录版本,包含图片与目录,记录缺章;取消、配额、重复请求与失败重试可控;通过 EPUB 检验工具并在外部阅读器打开。不能用导出来绕过站点访问限制。 | +| NAS 保存 | 为导出产物选择管理员允许的根目录,保存完成后再扫描入库 | 默认不覆盖同名文件;路径限制、空间不足、写入中断清理、并发导出和原子完成;数据库与书籍文件备份分别说明。 | + +2026-09-24 续批已实现 OPDS 服务端只读文件书籍及备份包 WebDAV 上传,使用边界见 [接入指南](opds-webdav.zh-CN.md)。进度互通、章节导出与 NAS 保存仍为独立方案,尚未实现。 + +## 浏览器验收范围 + +构建目标 `es2020` 仅描述语法,不保证浏览器具备所需 Web API。当前桌面验收使用 Playwright Chromium,具体版本及结果见 [P2 记录](p2-progress-2026-09-24.md)。 + +| 客户端 | 支持目标 / 验收要求 | 当前证据 | +| --- | --- | --- | +| Chrome / Edge | 当前稳定版与上一稳定版;阅读、登录、换源、下载和 IndexedDB | 自动化 Chromium 验收,不等同于全部 Chrome/Edge 版本实测 | +| Firefox | 当前稳定版与 ESR;特别检查排版、弹窗和离线存储 | 本轮未实机验收 | +| Safari / iOS | 当前与上一主版本;检查后台冻结、存储回收、语音和安全区域 | 本轮未实机验收,暂不承诺具体最低版本 | +| Android WebView | 当前稳定版;应用桥接、文件访问和生命周期另验收 | 本轮未设备验收 | +| 电纸书内置浏览器 | 必须先验证 JS、IndexedDB、Fetch/流、AbortController 与 dialog;使用电纸书预设 | 无实机证据,旧内核可能无法使用 | +| iOS 原生 / Windows ARM 原生 | 根据用户规模另评估;优先验证现有 Web 使用路径 | 未作为本轮开发项目 | + +离线应用安装和更新的环境要求参见 [PWA 指南](pwa.zh-CN.md)。设备低动画预设只关闭阅读翻页动画;其他界面动画可配合系统减少动态效果设置,无法代替硬件刷新控制。 diff --git a/docs/getting-started.en.md b/docs/getting-started.en.md index 3baf49c..98ab07b 100644 --- a/docs/getting-started.en.md +++ b/docs/getting-started.en.md @@ -4,6 +4,8 @@ This guide takes you through Docker deployment, account registration, and opening your first book. To run from source, use the [development guide](development.en.md). +This guide describes the current Node implementation. CI publishes `main` (development) and short commit tags, not `latest`; availability depends on a successful build. Pin a published commit tag for repeatable deployments. Legacy Java images, `/storage` data and Legado JSON sources are not directly compatible with this version. + ## 1. Prepare your environment - Install Docker Engine or Docker Desktop and the Docker Compose plugin. @@ -61,6 +63,14 @@ To allow later self-registration, set `ALLOW_REGISTRATION: "true"` in the Compos The health endpoint is `/api/v1/health`. For startup failures, missing books, or write errors, see [troubleshooting](configuration.en.md#troubleshooting). +If the image is unavailable, build the checked-out source: + +```sh +docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build +``` + +This still requires access to the Node base image and npm dependencies. Keep both `-f` arguments for subsequent management commands. For published images, set `READER_IMAGE=cnb.cool/hectorqin/reader:` in `.env` to pin a version. + ### Without Compose On Linux/macOS with a POSIX shell, you can also run the container using absolute mount paths: @@ -71,7 +81,7 @@ docker run -d --name reader --restart unless-stopped \ -p 8080:8080 \ -v /path/to/your/books:/books:ro \ -v "$(pwd)/data:/data" \ - cnb.cool/hectorqin/reader:latest + cnb.cool/hectorqin/reader:main ``` On Windows, the Compose setup above avoids shell-specific path and line-continuation syntax. @@ -90,6 +100,8 @@ As an administrator, add an OPDS source under Sources (书源) and configure its ### External source plugins +The generic plugin host does not parse Legado JSON. Do not enter a legacy JSON file or subscription URL as an npm package. Compatibility requires a separate plugin that explicitly supports that format; otherwise use local books or OPDS. + In Sources → Plugin management, administrators can confirm that they trust the plugin, then enter an npm package name (optionally with a version or tag) or upload a `.tgz` archive produced by `npm pack` (up to 100 MiB). Successful installations are enabled automatically; no manual deployment is needed. Add a source instance using the plugin’s source type and configure it from that instance’s management entry. One plugin can provide several independent instances. Capabilities and runtime dependencies are defined by each plugin. Plugins run with server process privileges. Only install trusted code. The server needs Node.js and npm, plus network access for npm packages and dependencies not bundled in uploaded archives. npm lifecycle scripts are disabled, so plugins must ship prebuilt runtime files. Installation files and temporary files stay in `DATA_DIR`, never in the books directory. @@ -98,6 +110,8 @@ See the [plugin protocol](source-plugins.md) and [extension pages](plugin-extens ## 5. Upgrade and back up +The [offline backup and restore tool (中文)](backup.zh-CN.md) checks file hashes and SQLite integrity and restores into a new directory. For older builds without this command, use a stopped-service copy as described below. + Back up both the book and data directories before upgrading. For a simple file-based SQLite backup, stop the service and copy the entire `/data` directory, including database auxiliary files, to avoid copying inconsistent live state. ```sh @@ -109,6 +123,8 @@ docker compose up -d Keep your data directory: it contains accounts, reading records, signing keys, and plugin state. Plugin packages are upgraded separately; disable the plugin in the UI before replacing its package. +When moving from the legacy Java version, keep a full backup and use a separate new data directory. Existing book files can be mounted, but accounts, shelves, progress, notes and source settings have no automatic migration tool. Do not copy legacy `/storage` over the new `/data`. Source-built deployments must rebuild using both Compose files instead of relying on `pull`. + ## Next steps - [User guide (中文)](user-guide.zh-CN.md): reading settings, shelves, sources, TTS, and offline access. diff --git a/docs/getting-started.zh-CN.md b/docs/getting-started.zh-CN.md index 0258913..ce4c1b5 100644 --- a/docs/getting-started.zh-CN.md +++ b/docs/getting-started.zh-CN.md @@ -4,6 +4,8 @@ 本文带你完成 Docker 部署、注册账号和读取第一本书。直接从源码运行请看[开发指南](development.zh-CN.md)。 +本指南对应当前 Node 版工作区。CI 发布 `main`(开发分支)和提交短哈希标签,没有发布 `latest` 的配置;标签可用性以对应构建成功为准。部署后核对版本,长期使用建议固定提交标签。旧 Java 版镜像、`/storage` 数据和 Legado JSON 书源不与新版直接兼容。 + ## 1. 准备环境 - 安装 Docker Engine 或 Docker Desktop,以及 Docker Compose 插件。 @@ -61,6 +63,14 @@ docker compose logs --tail=100 reader 健康检查地址为 `/api/v1/health`。若启动失败、目录为空或无法写入,请看[故障排查](configuration.zh-CN.md#故障排查)。 +若镜像尚未发布或无法拉取,可从当前检出的源码构建: + +```sh +docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build +``` + +此方式仍需能下载 Node 基础镜像和 npm 依赖。后续管理该部署时继续使用同样的两个 `-f` 参数。发布镜像部署可在 `.env` 设置 `READER_IMAGE=cnb.cool/hectorqin/reader:<已发布的提交标签>` 固定版本。 + ### 不使用 Compose 在 Linux/macOS 的 POSIX shell 中,也可使用绝对路径启动容器: @@ -71,7 +81,7 @@ docker run -d --name reader --restart unless-stopped \ -p 8080:8080 \ -v /path/to/your/books:/books:ro \ -v "$(pwd)/data:/data" \ - cnb.cool/hectorqin/reader:latest + cnb.cool/hectorqin/reader:main ``` Windows 推荐使用上面的 Compose 配置,避免不同 shell 的路径和换行语法差异。 @@ -90,6 +100,8 @@ Windows 推荐使用上面的 Compose 配置,避免不同 shell 的路径和 ### 外部书源插件 +通用来源插件不是 Legado JSON 解析器。旧版书源 JSON 或订阅链接不能直接当 npm 包安装;只有明确声明支持该格式的独立插件才能提供兼容,是否支持以该插件说明为准。尚无适用插件时,请使用本地书库或 OPDS。 + 管理员打开「书源 → 插件管理」,确认信任插件代码后,输入 npm 包名(可带版本或标签),或上传 `npm pack` 生成的 `.tgz` 安装包(最大 100 MiB),安装成功后自动启用。无需手动部署包目录。随后在「书源管理」选择插件提供的来源类型创建实例,从该实例的管理入口配置。一个插件可创建多个独立实例;具体功能与运行依赖由插件声明。 插件拥有服务端进程权限,请只安装可信代码。服务端需要 Node.js 和 npm;npm 包及未打包的依赖需要联网下载。安装期间禁用 npm 生命周期脚本,插件应预先构建并打包运行所需文件。所有安装文件与临时文件保存在 `DATA_DIR`,不会写入书库目录。 @@ -98,6 +110,8 @@ Windows 推荐使用上面的 Compose 配置,避免不同 shell 的路径和 ## 5. 更新与备份 +推荐使用[离线备份与恢复工具](backup.zh-CN.md),可验证文件清单和数据库完整性,并恢复到新目录。首次从没有维护命令的旧构建升级时,仍可按以下停机复制方式保留完整数据。 + 更新前备份书籍目录和数据目录。对 SQLite 采用简单文件备份时,先停止服务,再复制完整 `/data`(包括数据库及其辅助文件),避免复制运行中的不一致状态。 ```sh @@ -109,6 +123,8 @@ docker compose up -d 不要删除数据目录:其中包含账号、阅读记录、签名密钥和插件状态。插件包需单独升级;替换包前先在界面停用插件。 +从旧 Java 版迁移时,先保留其完整备份,使用独立的新数据目录部署新版。本地书文件可挂载到新书库;账号、书架、进度、笔记和旧书源配置目前没有自动迁移工具,不要直接用旧 `/storage` 覆盖新 `/data`。源码构建部署更新时使用两份 Compose 文件重新构建,不能仅执行 `pull`。 + ## 下一步 - [使用手册](user-guide.zh-CN.md):阅读设置、书架、书源、朗读和离线。 diff --git a/docs/opds-webdav.zh-CN.md b/docs/opds-webdav.zh-CN.md new file mode 100644 index 0000000..78aeb8f --- /dev/null +++ b/docs/opds-webdav.zh-CN.md @@ -0,0 +1,45 @@ +# OPDS 服务端与 WebDAV 备份 + +## 在其他阅读器中打开 Reader 书架 + +1. 登录 Reader,打开“书源与追更 → 搜书 → 连接外部阅读器”。 +2. 填写客户端名称并创建 OPDS 凭据。复制目录地址、OPDS 用户名和密码到支持 OPDS 1.2 / HTTP Basic 的阅读器。 +3. 密码只显示一次,有效期一年。丢失后撤销并创建新的;不使用 Reader 登录密码。 + +目录地址通常为 `https://你的域名/opds`。反代含路径前缀时,将 `PUBLIC_URL` 配置为完整外部地址,并确保反代转发 OPDS 路由与 Authorization 头。对外部署须使用 HTTPS。 + +首版提供当前账号未隐藏书架中的文件书籍、搜索、每页 50 本、封面与支持 Range 的原文件下载。章节书和目录漫画暂不发布。目录和下载请求都重新验证客户端凭据及账号状态,并设为 private/no-store。撤销或禁用账号后,新请求立即失效;已经下载到外部设备的文件无法远程删除。 + +只读应用凭据存储为 SHA-256 摘要,不可用于 Reader 普通 API 或写入。每个账号最多 20 个客户端。第三方客户端支持程度不同,本轮已对接 Reader 内置 OPDS 客户端,尚未对 Calibre、iOS 或电纸书客户端逐一实测。 + +接口: + +| 接口 | 认证 | 作用 | +| --- | --- | --- | +| GET/POST `/api/v1/opds/credentials` | Reader Bearer | 列出/创建自己的客户端凭据;创建返回一次性 password | +| DELETE `/api/v1/opds/credentials/:id` | Reader Bearer | 撤销自己的凭据 | +| GET `/opds?search=关键词&page=1` | OPDS Basic | Atom acquisition feed | +| GET `/opds/search.xml` | OPDS Basic | OpenSearch 描述 | +| GET `/opds/books/:id/content`、`/cover` | OPDS Basic | 原文件/封面,均检查账号书架权限 | + +## 上传一致性备份到 WebDAV + +这是管理员 CLI,上传已完成的备份目录;不直接同步运行中的 DATA_DIR,也不是 Legado 进度互通。先按 [备份指南](backup.zh-CN.md) 停止全部相关服务和插件、创建并验证备份。上传阶段可重新启动 Reader,备份目录保持不变。 + +在编译后的服务端目录运行: + +```sh +# 从安全的环境/秘密管理器注入,不要把密码写进仓库或 URL +export WEBDAV_URL='https://dav.example.com/reader-backups/' +export WEBDAV_USERNAME='backup-user' +# WEBDAV_PASSWORD 由运行环境注入 +node dist/maintenance/webdav-cli.js /backups/reader-20260924 reader-20260924.zip +``` + +目标 WebDAV collection 必须已存在,账号需有 HEAD、PUT、GET、MOVE、DELETE 权限。仅接受 HTTPS(loopback 测试允许 HTTP),不跟随重定向转发凭据。原始文件及最终 ZIP 默认限 2 GiB,可通过 `WEBDAV_MAX_BYTES` 设置;ZIP32 上限小于 4 GiB,最多 60000 条目录记录。打包使用本地临时磁盘,服务端需有足够空间。 + +流程为:验证目录 → 流式 ZIP 打包 → 再次验证目录 → 检查同名目标 → 上传随机临时文件 → 下载并核对大小/SHA-256 → `MOVE` 且 `Overwrite: F` 发布。暂时性 PUT 错误最多尝试三次;身份验证失败、校验失败或最终 MOVE 错误不会强行覆盖。最终发布结果不确定时须核对远端,不自动重试 MOVE。 + +取消/失败会尝试删除此次随机临时文件;网络持续故障时可能残留 `.reader-upload-*.zip`,管理员核对后清理。首版不自动删除历史备份、不设定计划任务。密码仅存在调用进程环境,不写入配置和日志。 + +恢复时,下载并在新的空目录解压 ZIP(Linux 上保留权限和内部符号链接),运行 `maintenance/cli.js verify`,通过后按备份指南恢复到新的 DATA_DIR。请使用可信工具解压;备份包含账号数据和 token.secret,应使用私有 WebDAV 存储及受控下载权限。 diff --git a/docs/p0-progress-2026-09-23.md b/docs/p0-progress-2026-09-23.md new file mode 100644 index 0000000..fbedd84 --- /dev/null +++ b/docs/p0-progress-2026-09-23.md @@ -0,0 +1,26 @@ +# P0 推进记录:第一批 + +范围来自 [issue 需求分析](closed-issues-analysis-2026-09-23.md)。本批没有改数据库 schema,也没有执行部署或操作现有用户数据。 + +## 已实现 + +1. 进度可靠性:后台/隐藏事件经过 App.flush,先等待阅读器待保存位置和本地写队列,再触发同步;打开书籍比较本地和服务端时间,保留较新的记录。服务端查询最长等待 5 秒,不可达时回退本地。存储写入错误向调用者报告,后续写入可重试,阅读器显示保存失败。 +2. 数据保护:服务端离线 backup/verify/restore 命令,完整 DATA_DIR 文件校验、SQLite 完整性检查、恢复到新目录、拒绝覆盖与路径重叠。未完成目录和备份目录不能直接启动服务。操作说明见 [备份恢复](backup.zh-CN.md)。 +3. 部署接入:按当前 CI 标签规则把 latest 改为 main,可通过 READER_IMAGE 固定版本;添加源码构建 Compose 覆盖文件;限制容器日志为 3 × 10 MiB;补充新旧版和 Legado 兼容范围。 + +## 验证方式 + +- Web 真实 ReaderScreen/OfflineStore 测试覆盖较新服务端、较新离线记录、网络失败、后台落盘顺序和读取期间不写临时位置。 +- 存储失败测试先观察到错误被吞,再修复并验证重试及 outbox 保留。 +- 服务端真实 SQLite 测试覆盖账号/进度/笔记/密钥/插件状态往返、损坏/缺失文件、路径篡改、恢复覆盖拒绝和启动保护。 +- Compose 默认和源码构建配置可解析;两端 TypeScript/构建检查。 + +## 未完成的 P0 范围 + +- 进度历史和用户主动恢复入口;多设备系统时间偏差下的冲突策略。 +- iOS Safari 与 Android 真机的手势、跨章、旋转、关闭/强制退出测试。本地 jsdom 测试不能替代真实渲染与系统生命周期。 +- 备份/恢复图形界面和运行中一致性快照;本批只提供需要停机的维护命令。 +- 首次接入向导、旧版数据自动迁移、数据损坏诊断界面。 +- 镜像拉取及真实容器升级演练:当前 Docker 引擎未运行,无法执行。Linux 符号链接和权限分支需在 Linux CI 验证。 + +当前实现仍以客户端 updatedAt 合并进度,不能据此承诺所有历史跳章/进度问题都已修复。 diff --git a/docs/p1-progress-2026-09-23.md b/docs/p1-progress-2026-09-23.md new file mode 100644 index 0000000..d24222d --- /dev/null +++ b/docs/p1-progress-2026-09-23.md @@ -0,0 +1,40 @@ +# P1 实施与验证记录(2026-09-23) + +本次实现原需求分析 D/E/F 的阅读工具主流程,并补充万本索引的查询基线。改动保留在工作区,未提交、未部署。 + +## 已实现 + +| 能力 | 交付行为 | +| --- | --- | +| 书内检索 | EPUB/TXT/远程章节逐章搜索、摘要、200 条上限、取消、命中高亮、返回原阅读位置;不对图片或扫描 PDF 做 OCR | +| 批注 | 正文选区高亮、批注、书签、编辑、删除、回正文;沿用现有离线同步和墓碑机制;修复同 ID 笔记跨账号覆盖 | +| EPUB 目录 | Web/服务端解析有序 nav/NCX 树,保留层级、fragment、spine,支持折叠和章内定位 | +| 个人纠错 | 精确文字替换、选区过滤、移除及撤销;按账号/书籍保存在 DATA_DIR/reader.db,版本冲突返回 409;不改原书 | +| TXT 目录 | 中文、英文、方括号模板和自定义标题前缀;预览后应用,规则持久化并可撤销 | +| 设备离线 | TXT/远程章节范围下载;EPUB/PDF/CBZ 整文件下载,保留 EPUB 内部资源;暂停、继续、取消;失败可重试并跳过完成资源 | +| 缓存管理 | 服务地址与账号隔离;默认 256 MiB 配额、实际资源字节占用、按书清理;配额写入串行,失败回滚旧资源 | +| 朗读诊断 | 当前引擎试听;HTTP 实际合成检测,分别报告配置、鉴权、响应体超时、非音频、空音频;停止和关面板释放试听 | +| 大书库基线 | 10,000 条索引的首尾分页、搜索、增量同步验证,单次查询按页返回 | + +主要实现位于 web/src/ui/reading-tools.tsx、text-anchor.ts、reading-overrides.ts、core/offline-download.ts、store/publications.ts,以及 server/src/services/reading-overrides.ts 和对应路由。 + +## 验证 + +- Web 全量 Vitest:40 个测试文件、585 项通过。此后新增 1 项下载重试/自定义 TXT 测试,相关 34 项定向回归通过。 +- Web Node 测试:29 项通过。 +- 服务端全量:338 项,336 通过、2 项 Windows 环境跳过;最后的覆盖层参数校验和万本测试再次通过。 +- 两端 TypeScript 检查、生产构建通过;git diff --check 通过。 +- 真实 Chromium + 真实 HTTP/SQLite + 生产 Web:320/390/1280 宽度无横向溢出;搜索高亮、返回、三级目录折叠/展开、选区批注、整 EPUB 缓存后阻断所有 API 并重开、纠错刷新保留/撤销、TXT 预览应用通过。 +- 浏览器验证脚本:web/tools/ui-review/p1-reading.mjs。在 web 运行 node --import tsx tools/ui-review/p1-reading.mjs;截图在 docs/ui-review/p1/。 +- 万本基线:内存 SQLite,10,000 条书籍/文件/书架记录;本机独立运行三次查询合计约 91ms。并发测试受负载影响,数值只用于基线,不作为 NAS/机械盘性能承诺。 + +## 明确边界 + +- 本轮完成的是上述可用流程;真实 iOS/Android、HTTPS 反代、后台音频和跨真机恢复尚待实机验收。 +- 未做磁盘上万本真实书籍的全量/增量扫描压测;内存索引基线不能替代该验收。 +- TXT 自定义规则是有界标题前缀匹配,不支持任意正则或跨书自动套用的规则库;变更章节结构后从首章打开,旧定位可能失效。 +- 纠错规则联网保存,已缓存规则离线应用;其他设备下次打开书籍拉取,不通过进度同步接口实时推送。空替换只过滤选区,未实现全书正则过滤。 +- 下载任务关闭面板即暂停。整文件下载重试从头请求;章节下载按已完成资源续传。图片目录不支持整书预下载。 +- 配额统计是新账号作用域下的正文/整文件缓存,未包含封面、服务端缓存和旧版本非账号隔离缓存;不自动淘汰。 +- 浏览器缓存可能被系统回收;未缓存的章节仍需联网。正文版本变化、目录重排或换源后的旧笔记不会强行匹配到不确定文本。 +- Docker 引擎未启动,本轮未做容器和实际部署验收。 diff --git a/docs/p2-progress-2026-09-24.md b/docs/p2-progress-2026-09-24.md new file mode 100644 index 0000000..9a384ca --- /dev/null +++ b/docs/p2-progress-2026-09-24.md @@ -0,0 +1,50 @@ +# P2 交付与验证记录 + +本轮依据 [需求分析](closed-issues-analysis-2026-09-23.md) F2、F3、G,实现书源质量与插件接入体验,并完成生态互通的独立方案。P0/P1 和其他已有改动保留;未提交、未部署。 + +## 已交付 + +1. **换源质量检测**:按所选章节获取并清洗正文,展示字符数、图片数、字节大小、最新章节和检测耗时;候选卡片保留本次检测摘要或错误。不批量预抓所有来源。切换章节或关闭面板会取消检测并忽略迟到结果。 +2. **空章保护**:刷新和确认换源时均强制验证,空白/清洗后为空的正文返回 `EMPTY_CHAPTER`;失败不替换当前绑定或已有缓存。质量预览不写正文缓存或修改书籍。图片章允许通过。 +3. **个人凭据状态**:增加当前账号的配置状态与最近访问观察,支持删除单项;保存、删除、修改连接配置后重置观察。用 generation 防止旧请求覆盖新凭据状态,不返回凭据值。 +4. **插件接入说明**:显示全部已知能力的“已声明/未声明”;站点登录过期、未登录、人工验证三个错误码不会清除 Reader 会话。说明 OPDS 客户端、网站适配、Legado 与宿主边界。 +5. **电纸书预设**:一键浅色、分页、无翻页动画,沿用当前设备设置存储,不影响其他设备。不是设备全刷控制。 +6. **生态方案**:[OPDS 服务端、WebDAV、进度互通、导出、NAS 与浏览器矩阵](ecosystem-roadmap.zh-CN.md)。这些协议仍需独立实施,本轮没有宣称已提供。 + +## 核心变更入口 + +- `server/src/publications/chapter-quality.ts`、`chapters.ts`:质量统计与读取/提交保护。 +- `server/src/services/source-access.ts`、`source-host.ts`、`db/schema.ts`:账号隔离的访问观察和凭据事务。 +- `server/src/http/routes/sources.ts`、`web/src/api/*`:质量接口、状态读取/删除与错误分类。 +- `web/src/ui/source-capabilities.tsx`、`sources-screen.tsx`:能力表及凭据页。 +- `web/src/ui/reader-chrome.tsx`、`reader-screen.tsx`:检测交互、取消与电纸书预设。 +- [API 说明](api.md)、[来源插件说明](source-plugins.md)、[使用手册](user-guide.zh-CN.md)。 + +## 验证 + +- Web TypeScript `tsc -p tsconfig.json --noEmit`、Vite 生产构建通过。 +- Server TypeScript `tsc -p tsconfig.build.json` 通过。 +- Web 全量 Vitest:41 个文件、594 项通过;独立 Node 测试 29 项通过。 +- Server 全量:342 项,其中 340 通过、2 项 Windows 平台跳过,0 失败。 +- 核心定向测试覆盖:Unicode/空白/HTML/图片章、质量预览不写入、空章不覆盖旧缓存或绑定、书籍所有权、凭据不泄漏、账号隔离、状态重置、旧请求失效、站点错误不退出 Reader、检测结果清理与迟到结果丢弃。 +- `web/tools/ui-review/p2-sources.mjs`:真实 HTTP + SQLite + 受控来源插件 + 生产 Web,Chromium **153.0.8010.12**。凭据保存/删除/状态重置、空章检测和确认换源失败、成功检测/换源、电纸书预设通过;320/390/1280 宽度无横向溢出,页面无未处理异常。 +- 最终文案和取消回归补充后:Web 定向 50 项、Server 来源 API 16 项再次通过,生产构建与浏览器流程再次通过。 +- `git diff --check` 通过。 + +截图见 [窄屏凭据页](ui-review/p2/credentials-320.png)、[窄屏质量检测](ui-review/p2/quality-320.png)、[桌面能力表](ui-review/p2/capabilities-1280.png)。 + +## 边界与后续 + +质量结果是一次读取快照,不能验证缺段、广告、标题错配或版权授权;确认换源会重新读取。耗时包括目录、正文、内嵌图片处理,不包括 acquire 和客户端网络往返。字符数不含空白和 HTML 标记。 + +访问状态表示来源实例最近一次请求观察;聚合插件的部分站点错误也会影响状态,不是逐网站会话管理。插件未报告过期时间时不推测,也不提供任意验证链接。人工验证仍需站点/插件实现。 + +浏览器流程使用受控插件,未对真实付费站点、验证码站点、Safari、Firefox、Android 或电纸书硬件做兼容认证。后续按生态方案单独验收。 + +## 续批:OPDS 服务端与 WebDAV(2026-09-24) + +已新增只读 OPDS 1.2 服务端、个人应用凭据管理界面、独立撤销/到期、账号隔离、分页/搜索/封面/Range 下载。与 Reader 内置 OPDS 客户端完成真实协议对接。 + +新增管理员 WebDAV 备份 CLI:离线快照验证、流式 ZIP、临时上传重试、远端 SHA-256 校验、MOVE 不覆盖发布、取消和失败清理。未配置用户 WebDAV 地址,因此未向用户外部存储上传;已用受控 HTTP WebDAV fixture 验证并解压校验备份。详见 [接入指南](opds-webdav.zh-CN.md)。 + +续批验证:Server 全量在 test-concurrency=2 下 344 项(342 通过、2 Windows 跳过),Web 595 项及已有 Node 29 项;类型与生产构建通过。默认高并发服务端运行曾使既有 1 秒插件超时测试失败,降低并发重跑全量通过。浏览器覆盖新 OPDS 凭据创建、仅一次显示、重新打开不回显、撤销后目录返回 401。 diff --git a/docs/source-plugins.md b/docs/source-plugins.md index 8e4a449..c475dca 100644 --- a/docs/source-plugins.md +++ b/docs/source-plugins.md @@ -8,7 +8,7 @@ 本文定义 reader 对本地书库、OPDS 书库和远程书源的统一扩展架构。目标是让核心程序只维护一套书架、阅读、缓存和任务逻辑;内置 `local`、`opds` 两种来源;远程书源以独立插件开发、安装和升级。 -本文区分目标架构和当前交付。local、OPDS 下载、Node 来源插件、配置界面、持久化章节出版物、富文本/图片缓存、手动刷新和服务端自动追更已经接通。外部来源通过通用进程协议接入。设备只缓存已读章节,整书预下载和缓存淘汰仍未实现。交付状态见 §13,调用示例见 [API 参考](api.md#来源与插件)。 +本文区分目标架构和当前交付。local、OPDS 下载、Node 来源插件、配置界面、持久化章节出版物、富文本/图片缓存、手动刷新和服务端自动追更已经接通。外部来源通过通用进程协议接入。P1 已增加用户主动预下载与配额检查,自动缓存淘汰仍未实现。交付状态见 §13,调用示例见 [API 参考](api.md#来源与插件)。 ## 1. 目标与范围 @@ -487,3 +487,13 @@ HTTP 返回宿主 DTO,凭据不回显。插件和来源配置管理需管理 来源实例可声明独立配置页面与后台任务,同一插件的多个实例互不共享配置。页面的 Tab、表单和输出由通用 DTO 定义,见[扩展协议](plugin-extensions.md)。 + +## P2 接入体验补充(2026-09-24) + +来源页按 descriptor 展示能力声明,不把声明当作站点可用性保证。个人凭据页显示配置状态和最近访问结果,用户可以编辑、保留或删除指定字段;不回显秘密。 + +插件遇到未登录、登录过期、人工验证时应分别抛出 AUTH_REQUIRED、AUTH_EXPIRED、VERIFICATION_REQUIRED(也可作为搜索部分失败项返回)。不要将登录页面当作成功正文返回。宿主不会识别任意站点的登录页面或自动绕过验证码;实际登录、验证码和网站适配仍由插件负责。未提供会话过期时间时,界面不估算过期时间。 + +换源的质量检测复用 acquire / getManifest / readResource,可能拉取内嵌图片;不要在这些读取调用中加入不可重入的副作用。检测结果是一次快照,确认换源时会重新读取并验证。宿主拒绝空白正文;正文正确性、缺段与广告识别仍须插件验证。 + +生态协议和客户端覆盖范围见 [独立方案](ecosystem-roadmap.zh-CN.md),交付证据见 [P2 记录](p2-progress-2026-09-24.md)。 diff --git a/docs/user-guide.zh-CN.md b/docs/user-guide.zh-CN.md index 63e8eb8..2227697 100644 --- a/docs/user-guide.zh-CN.md +++ b/docs/user-guide.zh-CN.md @@ -40,6 +40,8 @@ 字号、主题和界面设置按设备保存;阅读进度和笔记随账号同步。 +重新打开书籍时会比较本机与服务端进度,采用更新时间较新的位置;离线或服务端暂时不可达时使用本机记录。切到后台会先保存待写入的位置再尝试同步,关闭页面仍受浏览器生命周期限制。出现“阅读位置保存失败”时先保持页面打开,检查设备可用空间后重试,避免刷新丢失尚未保存的记录。 + | 设置 | 作用 | | --- | --- | | 分页 / 滚动 | 按屏翻页或连续阅读 | @@ -82,7 +84,7 @@ EPUB 章节按需加载,长书的目录与传输窗口分开处理,跨窗口 首个账号是管理员,后续成员默认由管理员创建。每个账号的书架、进度、笔记和高亮独立。阅读界面参数属于当前设备,不随账号覆盖其他设备的设置。 -离线时可使用已经缓存的内容,重新连通后同步阅读状态。远程章节当前以已读取章节缓存为主,不保证尚未读取的整本书可离线。停用或卸载插件后已有缓存可能仍可读,获取新内容需要恢复来源。整书预下载、缓存配额和自动淘汰尚未实现。 +离线时可使用已经缓存的内容,重新连通后同步阅读状态。在阅读器底部「工具 → 离线缓存」可下载远程章节/TXT 的指定范围,或下载完整 EPUB、PDF、CBZ;自定义目录的 TXT 下载完整文件。任务支持暂停、继续和取消,关闭面板会暂停,已下载内容保留。默认设备账号配额为 256 MiB,可调整或按书清理;不自动淘汰,也不删除原书、进度、笔记。服务端缓存与当前设备缓存独立,只有本设备完成下载的内容才能保证断网读取。停用来源后新内容需要恢复来源;图片目录暂不支持整书下载。 ## 进一步阅读 @@ -90,3 +92,20 @@ EPUB 章节按需加载,长书的目录与传输窗口分开处理,跨窗口 - [API:目录、资源、进度、上传和来源](api.md) - [数据边界与实现决策](architecture.md) - [书源插件扩展](plugin-extensions.md) + +## 阅读工具:搜索、笔记与内容整理 + +- **书内搜索**:打开「工具 → 书内搜索」,输入文字后逐章检索,最多展示 200 个命中,支持停止。点结果定位并高亮原文,再次打开工具可返回搜索前的位置。支持 EPUB/TXT/远程文字章节;扫描 PDF 和图片不提供 OCR。未缓存章节需要联网。 +- **高亮和批注**:先在正文选择文字,再打开「工具 → 笔记」,选择颜色并保存。填写批注即可形成笔记;无选区也可保存当前位置书签。支持编辑、删除、点击回正文,联网后沿用账号同步。原文变化导致定位不唯一时不会猜测位置。 +- **多级目录**:EPUB nav/NCX 保留三级及更深层级和章内锚点,有子项的目录可展开、折叠,选择章节后关闭面板。 +- **内容整理**:先选择文字,在「工具 → 内容整理」填写替换内容,留空可以过滤选区。修改以个人覆盖层保存在服务器 DATA_DIR 的 SQLite 中,不修改书籍文件。支持逐项移除和撤销上一次修改;跨设备并发保存冲突时需重新打开书籍。保存需联网,已缓存规则可离线应用。 +- **TXT 目录**:可选择中文章回、Chapter、方括号模板,或输入自定义标题前缀,预览后应用;留空恢复默认识别。规则匹配不超过 80 字的标题行,不执行任意正则。重新划分章节后从首章打开,旧笔记保留但可能需要重新定位。预览需要完整 TXT 文件。 +- **朗读检测**:按当前引擎试听。HTTP 引擎先实际请求合成,分别提示未配置、鉴权失败、超时、非音频和空音频。关闭面板或点击停止会释放试听;浏览器仍可能要求允许声音播放。 + +## 书源质量、登录状态与电纸书 + +在“切换书源”中选择来源与章节,点击“检测所选章节 / 重试”,可查看字符数、图片数、大小和检测耗时。空章会给出错误;检测不会切换当前书籍。关闭选章弹窗后,候选列表保留本次检测摘要。确认换源仍会再次验证,失败保留原内容。检测通过不保证章节完整或与原书一致。 + +在来源页展开“插件能力与兼容性”,查看当前插件声明的能力;打开“登录凭据”查看哪些字段已配置和最近访问结果。未编辑字段会保留,清空后保存会删除。保存成功只代表已存储,请重新搜索或浏览来检查访问。需要人工验证时,到站点或插件提供的管理页面处理后重试,不必退出 Reader 账号。 + +阅读设置中的“应用电纸书预设”会切换为浅色、分页和无翻页动画,仅影响当前设备;其他选项仍可调整。该预设不控制电纸书全刷或硬件残影。浏览器验收范围与 OPDS/WebDAV 后续边界见 [生态互通方案](ecosystem-roadmap.zh-CN.md)。 diff --git a/server/src/config/index.ts b/server/src/config/index.ts index ea41f00..2c2ca13 100644 --- a/server/src/config/index.ts +++ b/server/src/config/index.ts @@ -98,6 +98,9 @@ function loadOrCreateSecret(dataDir: string, file = 'token.secret'): string { export function loadConfig(): AppConfig { const booksDir = resolve(process.env.BOOKS_DIR ?? '/books'); const dataDir = resolve(process.env.DATA_DIR ?? join(process.cwd(), DATA_DIR_DEFAULT_NAME)); + if (existsSync(join(dataDir, '.reader-backup-incomplete')) || existsSync(join(dataDir, 'reader-backup.json'))) { + throw new Error('DATA_DIR is an incomplete operation or backup directory; restore a verified backup to a new directory before starting.'); + } const dataRelativeToBooks = relative(booksDir, dataDir); const insideBooks = dataRelativeToBooks === '' || ( dataRelativeToBooks !== '..' && !dataRelativeToBooks.startsWith(`..${sep}`) && !isAbsolute(dataRelativeToBooks) diff --git a/server/src/db/schema.ts b/server/src/db/schema.ts index 5bcdeb1..e2497ef 100644 --- a/server/src/db/schema.ts +++ b/server/src/db/schema.ts @@ -243,4 +243,23 @@ CREATE TABLE IF NOT EXISTS metadata_overrides ( updated_at INTEGER NOT NULL, PRIMARY KEY (book_id, field) ); +CREATE TABLE IF NOT EXISTS reading_overrides (user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, book_id TEXT NOT NULL REFERENCES books(id) ON DELETE CASCADE, version INTEGER NOT NULL, payload TEXT NOT NULL, PRIMARY KEY(user_id,book_id,version)); + +CREATE TABLE IF NOT EXISTS opds_credentials ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + token_hash TEXT NOT NULL UNIQUE, + created_at INTEGER NOT NULL, + expires_at INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS source_access ( + source_id TEXT NOT NULL REFERENCES source_instances(id) ON DELETE CASCADE, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + generation INTEGER NOT NULL DEFAULT 0, + state TEXT NOT NULL DEFAULT 'unknown', + checked_at INTEGER, + PRIMARY KEY(source_id,user_id) +); `; diff --git a/server/src/http/app.ts b/server/src/http/app.ts index 914cfe7..612fb45 100644 --- a/server/src/http/app.ts +++ b/server/src/http/app.ts @@ -5,9 +5,11 @@ import { registerErrorHandler } from './errors.ts'; import { registerAuthRoutes } from './routes/auth.ts'; import { registerLibraryRoutes } from './routes/library.ts'; import { registerSyncRoutes } from './routes/sync.ts'; +import { registerReadingOverrideRoutes } from './routes/reading-overrides.ts'; import { registerTtsRoutes } from './routes/tts.ts'; import { registerWebRoutes } from './routes/web.ts'; import { registerSourceRoutes } from './routes/sources.ts'; +import { registerOpdsRoutes } from './routes/opds.ts'; import { isOriginAllowed, resolveCorsOrigin } from './cors.ts'; export function buildApp(ctx: AppContext): FastifyInstance { @@ -62,7 +64,9 @@ export function buildApp(ctx: AppContext): FastifyInstance { registerLibraryRoutes(app, ctx); registerSyncRoutes(app, ctx); registerTtsRoutes(app, ctx); + registerReadingOverrideRoutes(app, ctx); registerSourceRoutes(app, ctx); + registerOpdsRoutes(app, ctx); // Registered last: the SPA fallback must not shadow an API route. registerWebRoutes(app, ctx); diff --git a/server/src/http/routes/opds.ts b/server/src/http/routes/opds.ts new file mode 100644 index 0000000..3a8c90c --- /dev/null +++ b/server/src/http/routes/opds.ts @@ -0,0 +1,94 @@ +import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; +import { createReadStream } from 'node:fs'; +import { realpath, stat } from 'node:fs/promises'; +import { isAbsolute, relative } from 'node:path'; +import type { AppContext } from '../context.ts'; +import { authenticate, currentUser } from '../auth.ts'; +import { OpdsCredentials, xml } from '../../services/opds.ts'; +import { FilePublications } from '../../publications/files.ts'; +import { allFileHandlers, contentTypeFor } from '../../indexer/formats/index.ts'; +import { badRequest, notFound } from '../../lib/errors.ts'; +import { assertSafeRel, resolveInside } from '../../lib/paths.ts'; +import { sendAssetPayload } from '../assets.ts'; + +const feedType = 'application/atom+xml;profile=opds-catalog;kind=acquisition'; + +export function registerOpdsRoutes(app: FastifyInstance, ctx: AppContext): void { + const credentials = new OpdsCredentials(ctx.db), auth = authenticate(ctx); + const formats = allFileHandlers().map(handler => handler.format); + const root = `${ctx.config.publicUrl.replace(/\/$/, '')}/opds`; + const access = async (request: FastifyRequest, reply: FastifyReply) => { + try { + const id = credentials.authenticate(request.headers.authorization); + const user = ctx.users.byId(id)!; + request.currentUser = { id, username: user.username, displayName: user.display_name, role: user.role, createdAt: user.created_at }; + } catch (error) { reply.header('www-authenticate', 'Basic realm="Reader OPDS", charset="UTF-8"'); throw error; } + }; + app.addHook('onSend', async (request, reply, payload) => { + if (request.url.startsWith('/opds') || request.url.startsWith('/api/v1/opds/')) { + reply.header('cache-control', 'private, no-store'); reply.header('referrer-policy', 'no-referrer'); + } + return payload; + }); + app.get('/api/v1/opds/credentials', { preHandler: auth }, async request => ({ credentials: credentials.list(currentUser(request).id), catalogUrl: root })); + app.post('/api/v1/opds/credentials', { preHandler: auth }, async (request, reply) => { + reply.code(201); + return { ...credentials.create(currentUser(request).id, (request.body as { name?: unknown } | null)?.name), catalogUrl: root }; + }); + app.delete('/api/v1/opds/credentials/:id', { preHandler: auth }, async request => { + credentials.revoke(currentUser(request).id, (request.params as { id: string }).id); return { ok: true }; + }); + app.get('/opds/', { preHandler: access }, async (request, reply) => reply.redirect(root + (request.url.includes('?') ? request.url.slice(request.url.indexOf('?')) : ''))); + app.get('/opds', { preHandler: access }, async (request, reply) => { + const query = request.query as { page?: unknown; search?: unknown }; + const page = query.page === undefined ? 1 : Number(query.page); + if (!Number.isSafeInteger(page) || page < 1 || page > 100000 || + (query.search !== undefined && (typeof query.search !== 'string' || query.search.length > 200))) throw badRequest('invalid OPDS query'); + const search = query.search as string | undefined; + const result = ctx.shelf.list(currentUser(request).id, { page, pageSize: 50, fileFormats: formats, sort: 'title', ...(search ? { search } : {}) }); + const link = (rel: string, href: string, type = feedType) => ``; + const pageUrl = (number: number) => root + '?' + new URLSearchParams({ page: String(number), ...(search ? { search } : {}) }); + const entries = result.items.map(book => { + const href = `${root}/books/${encodeURIComponent(book.id)}`; + const file = new FilePublications(ctx.db, ctx.config).context(book.id); + return `urn:reader:book:${xml(book.id)}${xml(book.title)}${new Date(book.updatedAt).toISOString()} + ${xml(book.author)}${xml(book.description)} + ${link('http://opds-spec.org/acquisition', href + '/content', contentTypeFor(file.relPath))} + ${book.coverUrl ? link('http://opds-spec.org/image', href + '/cover', '') : ''}`; + }).join(''); + return reply.type(feedType).send(` + urn:reader:opds:${xml(currentUser(request).id)}Reader 个人书架${new Date().toISOString()} + ${link('self', pageUrl(page))}${link('start', root)}${link('search', root + '/search.xml', 'application/opensearchdescription+xml')} + ${page > 1 ? link('previous', pageUrl(page - 1)) : ''}${page * result.pageSize < result.total ? link('next', pageUrl(page + 1)) : ''} + ${result.total}${(page - 1) * result.pageSize + 1}${result.pageSize}${entries}`); + }); + app.get('/opds/search.xml', { preHandler: access }, async (_request, reply) => reply.type('application/opensearchdescription+xml').send( + `Reader搜索个人书架UTF-8`, + )); + app.get('/opds/books/:id/content', { preHandler: access }, async (request, reply) => { + const book = ctx.shelf.get(currentUser(request).id, (request.params as { id: string }).id); + if (!formats.includes(book.format)) throw notFound('publication not available'); + const files = new FilePublications(ctx.db, ctx.config); + await containedFile(files.context(book.id).absPath, [ctx.config.booksDir, ctx.config.dataDir]); + return sendAssetPayload(request, reply, await files.content(book.id, book.format)); + }); + app.get('/opds/books/:id/cover', { preHandler: access }, async (request, reply) => { + const book = ctx.shelf.get(currentUser(request).id, (request.params as { id: string }).id); + if (!formats.includes(book.format)) throw notFound('publication not available'); + const row = ctx.db.get<{ cover_path: string | null }>('SELECT cover_path FROM books WHERE id=?', book.id); + if (!row?.cover_path) throw notFound('cover unavailable'); + const path = resolveInside(ctx.config.dataDir, assertSafeRel(row.cover_path)); + await containedFile(path, [ctx.config.dataDir]); + return reply.type(contentTypeFor(path)).send(createReadStream(path)); + }); +} + +async function containedFile(path: string, roots: string[]): Promise { + let actual: string; + try { actual = await realpath(path); } catch { throw notFound('file unavailable'); } + for (const root of roots) { + const rel = relative(await realpath(root), actual); + if (rel && !isAbsolute(rel) && rel !== '..' && !rel.startsWith('../') && !rel.startsWith('..\\') && (await stat(actual)).isFile()) return; + } + throw notFound('file unavailable'); +} diff --git a/server/src/http/routes/reading-overrides.ts b/server/src/http/routes/reading-overrides.ts new file mode 100644 index 0000000..418340c --- /dev/null +++ b/server/src/http/routes/reading-overrides.ts @@ -0,0 +1,19 @@ +import type { FastifyInstance } from 'fastify'; +import type { AppContext } from '../context.ts'; +import { authenticate, currentUser } from '../auth.ts'; +import { ReadingOverrideService, type ReadingOverrides } from '../../services/reading-overrides.ts'; +import { badRequest } from '../../lib/errors.ts'; +export function registerReadingOverrideRoutes(app: FastifyInstance, ctx: AppContext): void { + const service = new ReadingOverrideService(ctx.db), auth = authenticate(ctx); + const access = (request: Parameters>[0]) => { + const user = currentUser(request), { id } = request.params as { id: string }; + ctx.shelf.get(user.id,id); return { userId:user.id,bookId:id }; + }; + app.get('/api/v1/books/:id/reading-overrides',{preHandler:auth},async request => { const {userId,bookId}=access(request); return service.get(userId,bookId); }); + app.put('/api/v1/books/:id/reading-overrides',{preHandler:auth},async request => { const {userId,bookId}=access(request); return service.save(userId,bookId,request.body as ReadingOverrides); }); + app.post('/api/v1/books/:id/reading-overrides/undo',{preHandler:auth},async request => { + const {userId,bookId}=access(request); const version = (request.body as {version?:number}|null)?.version; + if (!Number.isSafeInteger(version) || !version || version < 1) throw badRequest('invalid version'); + return service.undo(userId,bookId,version); + }); +} diff --git a/server/src/http/routes/sources.ts b/server/src/http/routes/sources.ts index 6874994..8f27833 100644 --- a/server/src/http/routes/sources.ts +++ b/server/src/http/routes/sources.ts @@ -84,6 +84,10 @@ export function registerSourceRoutes(app: FastifyInstance, ctx: AppContext): voi host.remove((request.params as { id: string }).id); return { ok: true }; }); + app.get('/api/v1/sources/:id/credentials', {preHandler:auth}, async request => host.credentialStatus((request.params as {id:string}).id,currentUser(request).id)); + app.delete('/api/v1/sources/:id/credentials/:key', {preHandler:auth}, async request => { + const {id,key} = request.params as {id:string;key:string}; host.deleteCredential(id,currentUser(request).id,key); return {ok:true}; + }); app.put('/api/v1/sources/:id/credentials/:key', { preHandler: auth }, async (request) => { const user = currentUser(request); const { id, key } = request.params as { id: string; key: string }; @@ -173,6 +177,11 @@ export function registerSourceRoutes(app: FastifyInstance, ctx: AppContext): voi const ref = textBody(request.body, 'entryRef'); return withSignal(request, reply, signal => host.switchPreview(user.id, id, ref, signal)); }); + app.post('/api/v1/books/:id/switch-quality', { preHandler: auth }, async (request, reply) => { + const user = currentUser(request), { id } = request.params as { id: string }; ctx.shelf.get(user.id,id); + const ref = textBody(request.body,'entryRef'), chapter = textBody(request.body,'chapterId'); + return withSignal(request,reply,signal => host.switchQuality(user.id,id,ref,chapter,signal)); + }); app.post('/api/v1/books/:id/switch-source', { preHandler: auth }, async (request, reply) => { const user = currentUser(request), { id } = request.params as { id: string }; ctx.shelf.get(user.id, id); const ref = textBody(request.body, 'entryRef'), chapter = textBody(request.body, 'chapterId'), revision = textBody(request.body, 'revision'); diff --git a/server/src/http/routes/tts.ts b/server/src/http/routes/tts.ts index 92d51fb..2b3d5f9 100644 --- a/server/src/http/routes/tts.ts +++ b/server/src/http/routes/tts.ts @@ -36,6 +36,18 @@ export function registerTtsRoutes(app: FastifyInstance, ctx: AppContext): void { return { ...capabilities, voices: await ctx.tts.voices() }; }); + app.post('/api/v1/tts/test', { preHandler: auth }, async request => { + const input = request.body as { voice?: unknown; speed?: unknown } | null; + if (input?.voice !== undefined && typeof input.voice !== 'string') throw badRequest('voice must be a string'); + if (input?.speed !== undefined && (typeof input.speed !== 'number' || !Number.isFinite(input.speed))) throw badRequest('speed must be a number'); + const started = Date.now(); + const audio = await ctx.tts.synthesize({ text: '这是阅读器朗读试听。愿你享受阅读的时光。', + ...(typeof input?.voice === 'string' ? { voice: input.voice } : {}), + ...(typeof input?.speed === 'number' ? { speed: input.speed } : {}), + }); + return { bytes: audio.bytes.length, contentType: audio.contentType, elapsedMs: Date.now() - started, cached: audio.cached }; + }); + app.get('/api/v1/tts', { preHandler: auth }, async (request, reply) => { const query = request.query as Record; const text = query.text ?? ''; diff --git a/server/src/indexer/formats/epub.ts b/server/src/indexer/formats/epub.ts index 2b00b74..c3219e4 100644 --- a/server/src/indexer/formats/epub.ts +++ b/server/src/indexer/formats/epub.ts @@ -13,6 +13,7 @@ import { } from './registry.ts'; import { parseEpub, extractEpubCover } from '../metadata.ts'; import { XMLParser } from 'fast-xml-parser'; +import { parseNavigation } from './navigation.ts'; /** * EPUB 2/3. @@ -153,6 +154,7 @@ export const epubHandler = registerFileHandler({ const archive = await ZipArchive.open(ctx.absPath); const pkg = await readPackage(archive); const headingCache = new Map(); + if (pkg.toc.length) return pkg.toc; /** * A chapter's own first heading, read only when the package gave no title. @@ -334,6 +336,7 @@ interface SpineItem { interface Package { spine: SpineItem[]; titles: Map; + toc: TocEntry[]; } /** @@ -358,7 +361,7 @@ async function readPackage(archive: ZipArchive): Promise { * being used as a fallback. */ async function readPackageWith(archive: ZipArchive, withTitles: boolean): Promise { - const empty: Package = { spine: [], titles: new Map() }; + const empty: Package = { spine: [], titles: new Map(), toc: [] }; const containerEntry = archive.get('META-INF/container.xml'); let opfPath = ''; if (containerEntry) { @@ -401,9 +404,30 @@ async function readPackageWith(archive: ZipArchive, withTitles: boolean): Promis * is what this did) threw all three away for any book whose titles could not * be read. */ - if (!withTitles) return { spine, titles: new Map() }; + if (!withTitles) return { spine, titles: new Map(), toc: [] }; + const declared = asArray(pkg?.manifest?.item); + const nav = declared.find(item => String(item?.['@_properties'] ?? '').split(/\s+/).includes('nav')); + const ncx = declared.find(item => item?.['@_media-type'] === 'application/x-dtbncx+xml'); + let toc: TocEntry[] = []; + for (const [item, kind] of [[nav, 'nav'], [ncx, 'ncx']] as const) { + if (!item) continue; + const path = resolveHref(base, String(item['@_href'])); + try { + const entries = parseNavigation((await archive.read(path, CHAPTER_PROBE_BYTES * 4)).toString('utf8'), kind); + toc = entries.flatMap(entry => { + const hash = entry.href.indexOf('#'); + const href = hash < 0 ? entry.href : entry.href.slice(0, hash); + const fragment = hash < 0 ? '' : entry.href.slice(hash); + const resolved = href ? resolveHref(posix.dirname(path), href) : path; + const index = spine.findIndex(s => s.path === resolved); + return index < 0 ? [] : [{ href: `xhtml:${resolved}${fragment}`, title: entry.title, level: entry.depth, spine: index }]; + }); + if (toc.length) break; + } catch { /* Fall back to NCX or spine titles for malformed navigation. */ } + } const titles = await readTitles(archive, base).catch(() => new Map()); - return { spine, titles }; + for (const entry of toc) if (!titles.has(spine[entry.spine!]!.path)) titles.set(spine[entry.spine!]!.path, entry.title); + return { spine, titles, toc }; } /** diff --git a/server/src/indexer/formats/navigation.ts b/server/src/indexer/formats/navigation.ts new file mode 100644 index 0000000..6ce7ff4 --- /dev/null +++ b/server/src/indexer/formats/navigation.ts @@ -0,0 +1,55 @@ +import { XMLParser } from 'fast-xml-parser'; + +export interface NavigationItem { href: string; title: string; depth: number } +type Node = Record; +const parser = new XMLParser({ preserveOrder: true, ignoreAttributes: false, removeNSPrefix: true, processEntities: false, trimValues: false, parseTagValue: false }); +const decode = (text: string) => text.replace(/&#(x[0-9a-f]+|\d+);|&(amp|lt|gt|quot|apos);/gi, (all, number: string, name: string) => { + if (!number) return ({ amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" } as Record)[name.toLowerCase()] ?? all; + const code = parseInt(number[0]?.toLowerCase() === 'x' ? number.slice(1) : number, number[0]?.toLowerCase() === 'x' ? 16 : 10); + return code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : all; +}); +const textOf = (nodes: Node[]): string => nodes.map(node => Object.entries(node).filter(([key]) => key !== ':@').map(([key, value]) => key === '#text' ? String(value) : Array.isArray(value) ? textOf(value) : '').join('')).join(''); +function find(nodes: Node[], tag: string): Node[] { + const result: Node[] = []; + for (const node of nodes) for (const [key, children] of Object.entries(node)) { + if (key === tag) result.push(node); + if (Array.isArray(children)) result.push(...find(children, tag)); + } + return result; +} + +/** Ordered XML tree traversal preserves nested navPoint/li entries and fragment hrefs. */ +export function parseNavigation(xml: string, kind: 'nav' | 'ncx'): NavigationItem[] { + const tree = parser.parse(xml) as Node[]; + const result: NavigationItem[] = []; + if (kind === 'nav') { + const navs = find(tree, 'nav'); + const nav = navs.find(n => String(n[':@']?.['@_type'] ?? '').split(/\s+/).includes('toc')) ?? navs[0]; + if (!nav) return []; + const walk = (nodes: Node[], depth: number): void => { + for (const node of nodes) for (const [tag, children] of Object.entries(node)) { + if (!Array.isArray(children)) continue; + if (tag === 'a') { + const href = node[':@']?.['@_href']; + const title = decode(textOf(children)).trim(); + if (typeof href === 'string' && title) result.push({ href: decode(href), title, depth: Math.max(0, depth - 1) }); + } else walk(children, depth + (tag === 'li' ? 1 : 0)); + } + }; + walk(nav.nav, 0); + } else { + const map = find(tree, 'navMap')[0]; + const walk = (nodes: Node[], depth: number): void => { + for (const node of nodes) if (Array.isArray(node.navPoint)) { + const label = node.navPoint.find((n: Node) => n.navLabel); + const content = node.navPoint.find((n: Node) => n.content); + const href = content?.[':@']?.['@_src']; + const title = label ? decode(textOf(label.navLabel)).trim() : ''; + if (typeof href === 'string' && title) result.push({ href: decode(href), title, depth }); + walk(node.navPoint, depth + 1); + } + }; + if (map) walk(map.navMap, 0); + } + return result; +} diff --git a/server/src/maintenance/backup.ts b/server/src/maintenance/backup.ts new file mode 100644 index 0000000..b62be0c --- /dev/null +++ b/server/src/maintenance/backup.ts @@ -0,0 +1,138 @@ +import { createHash } from 'node:crypto'; +import { createReadStream } from 'node:fs'; +import { chmod, copyFile, lstat, mkdir, readFile, readdir, readlink, realpath, symlink, unlink, writeFile } from 'node:fs/promises'; +import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'; +import { DatabaseSync } from 'node:sqlite'; + +const MANIFEST = 'reader-backup.json'; +const INCOMPLETE = '.reader-backup-incomplete'; +interface Entry { path: string; kind: 'file' | 'directory' | 'link'; size?: number; sha256?: string; target?: string } +export interface BackupManifest { format: 'reader-data-backup'; version: 1; createdAt: string; entries: Entry[] } + +function inside(root: string, path: string): boolean { + const rel = relative(root, path); + return rel === '' || (!isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`)); +} + +async function digest(path: string): Promise { + const hash = createHash('sha256'); + for await (const chunk of createReadStream(path)) hash.update(chunk); + return hash.digest('hex'); +} + +async function inventory(root: string, rel = ''): Promise { + const entries: Entry[] = []; + for (const name of (await readdir(join(root, rel))).sort()) { + // SQLite rebuilds shared memory from the database/WAL. Reader locks in this + // file can change even during a read-only integrity check. + if (!rel && (name === MANIFEST || name === INCOMPLETE || name === 'reader.db-shm')) continue; + const path = rel ? `${rel}/${name}` : name; + const absolute = join(root, path); + const stat = await lstat(absolute); + if (stat.isSymbolicLink()) { + const target = await readlink(absolute); + if (isAbsolute(target) || !inside(root, resolve(dirname(absolute), target)) || !inside(root, await realpath(absolute))) { + throw new Error(`不支持指向数据目录外的符号链接:${path}`); + } + entries.push({ path, kind: 'link', target }); + } else if (stat.isDirectory()) { + entries.push({ path, kind: 'directory' }); + entries.push(...await inventory(root, path)); + } else if (stat.isFile()) { + entries.push({ path, kind: 'file', size: stat.size, sha256: await digest(absolute) }); + } else { + throw new Error(`数据目录包含不支持的文件类型:${path}`); + } + } + return entries; +} + +function checkDatabase(root: string): void { + // Read the existing schema without running migrations or creating a database. + const db = new DatabaseSync(join(root, 'reader.db'), { readOnly: true }); + try { + const rows = db.prepare('PRAGMA integrity_check').all(); + if (rows.length !== 1 || rows[0]?.integrity_check !== 'ok') throw new Error('SQLite 完整性校验失败'); + if (db.prepare('PRAGMA foreign_key_check').get()) throw new Error('SQLite 外键完整性校验失败'); + for (const table of ['users', 'books', 'reading_progress', 'notes']) { + if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table)) { + throw new Error(`不是当前 reader 数据库:缺少 ${table}`); + } + } + } finally { db.close(); } +} + +async function newDestination(source: string, destination: string): Promise { + const parent = await realpath(dirname(resolve(destination))); + const target = join(parent, relative(dirname(resolve(destination)), resolve(destination))); + if (inside(source, target) || inside(target, source)) throw new Error('备份/恢复目录不能与源目录重叠'); + // Exclusive creation: never merge into or overwrite existing user data. + await mkdir(target, { mode: 0o700 }); + await writeFile(join(target, INCOMPLETE), '操作未完成,请勿用此目录启动服务。\n', { flag: 'wx', mode: 0o600 }); + return target; +} + +async function copyEntries(source: string, target: string, entries: Entry[]): Promise { + for (const entry of entries) { + const from = join(source, entry.path), to = join(target, entry.path); + if (entry.kind === 'directory') await mkdir(to, { mode: 0o700 }); + else if (entry.kind === 'file') { + await copyFile(from, to); + // Preserve plugin executables without making credentials world-readable. + await chmod(to, (await lstat(from)).mode & 0o700); + } + } + // Create links last, after all their targets, and never traverse them while copying. + for (const entry of entries.filter(value => value.kind === 'link')) { + await symlink(entry.target!, join(target, entry.path), 'file'); + } +} + +/** Offline only: stop every server/plugin process using DATA_DIR before calling. */ +export async function createBackup(dataDir: string, destination: string): Promise { + const source = await realpath(dataDir); + if ((await readdir(source)).some(name => name === INCOMPLETE || name === MANIFEST)) { + throw new Error('源目录是备份或未完成操作目录,不能作为运行数据目录备份'); + } + checkDatabase(source); + const entries = await inventory(source); + const target = await newDestination(source, destination); + await copyEntries(source, target, entries); + if (JSON.stringify(await inventory(source)) !== JSON.stringify(entries) || + JSON.stringify(await inventory(target)) !== JSON.stringify(entries)) { + throw new Error('复制期间数据发生变化或校验失败;请停止服务后重新备份到新目录'); + } + checkDatabase(target); + const manifest: BackupManifest = { format: 'reader-data-backup', version: 1, createdAt: new Date().toISOString(), entries }; + await writeFile(join(target, MANIFEST), JSON.stringify(manifest, null, 2), { flag: 'wx', mode: 0o600 }); + await unlink(join(target, INCOMPLETE)); + return manifest; +} + +export async function verifyBackup(directory: string): Promise { + const source = await realpath(directory); + if ((await readdir(source)).includes(INCOMPLETE)) throw new Error('备份未完成,拒绝恢复'); + const manifest = JSON.parse(await readFile(join(source, MANIFEST), 'utf8')) as BackupManifest; + if (manifest.format !== 'reader-data-backup' || manifest.version !== 1 || !Array.isArray(manifest.entries)) { + throw new Error('不支持的备份格式'); + } + // Rebuild from actual files, not manifest paths: a modified manifest cannot + // instruct restore to write outside the new data directory. + if (JSON.stringify(await inventory(source)) !== JSON.stringify(manifest.entries)) { + throw new Error('备份文件校验失败:文件缺失、多余或内容已改变'); + } + checkDatabase(source); + return manifest; +} + +/** Restore to a new directory; switching the deployed DATA_DIR remains explicit. */ +export async function restoreBackup(directory: string, destination: string): Promise { + const source = await realpath(directory); + const manifest = await verifyBackup(source); + const target = await newDestination(source, destination); + await copyEntries(source, target, manifest.entries); + if (JSON.stringify(await inventory(target)) !== JSON.stringify(manifest.entries)) throw new Error('恢复后文件校验失败'); + checkDatabase(target); + await unlink(join(target, INCOMPLETE)); + return manifest; +} diff --git a/server/src/maintenance/cli.ts b/server/src/maintenance/cli.ts new file mode 100644 index 0000000..41f5204 --- /dev/null +++ b/server/src/maintenance/cli.ts @@ -0,0 +1,30 @@ +import { createBackup, restoreBackup, verifyBackup } from './backup.ts'; + +const HELP = `reader 离线数据维护(不包含 BOOKS_DIR 原书) + backup <新备份目录> --server-stopped + verify <备份目录> + restore <备份目录> <新DATA_DIR> --server-stopped + +backup/restore 前必须停止使用该数据目录的所有服务和插件。 +--server-stopped 是操作者确认,不是运行状态检测。目标目录必须不存在,父目录必须存在。 +失败时保留带 .reader-backup-incomplete 的目录供检查,请勿启动该目录。`; + +async function main(): Promise { + const [command, source, target, confirmation, ...extra] = process.argv.slice(2); + if (!command || command === '--help') { console.log(HELP); return; } + if (!source || extra.length || !['backup', 'verify', 'restore'].includes(command) || + (command === 'verify' ? !!target : !target || confirmation !== '--server-stopped')) { + throw new Error(HELP); + } + const result = command === 'verify' ? await verifyBackup(source) + : command === 'backup' ? await createBackup(source, target!) : await restoreBackup(source, target!); + console.log(JSON.stringify({ operation: command, createdAt: result.createdAt, + files: result.entries.filter(entry => entry.kind === 'file').length, + bytes: result.entries.reduce((sum, entry) => sum + (entry.size ?? 0), 0), + destination: target ?? source }, null, 2)); +} + +main().catch((error: unknown) => { + console.error(error instanceof Error ? error.message : '数据维护失败'); + process.exitCode = 1; +}); diff --git a/server/src/maintenance/webdav-cli.ts b/server/src/maintenance/webdav-cli.ts new file mode 100644 index 0000000..f9d16cd --- /dev/null +++ b/server/src/maintenance/webdav-cli.ts @@ -0,0 +1,19 @@ +import { uploadBackup } from './webdav.ts'; + +const controller = new AbortController(); +process.once('SIGINT', () => controller.abort()); +process.once('SIGTERM', () => controller.abort()); +const [directory, name, ...extra] = process.argv.slice(2); +if (!directory || !name || extra.length) { + console.error('用法:node dist/maintenance/webdav-cli.js <已验证备份目录> <远端名称.zip>\n环境变量:WEBDAV_URL、WEBDAV_USERNAME、WEBDAV_PASSWORD;可选 WEBDAV_MAX_BYTES。'); + process.exitCode = 1; +} else { + uploadBackup(directory, { name, url: process.env.WEBDAV_URL ?? '', username: process.env.WEBDAV_USERNAME ?? '', + password: process.env.WEBDAV_PASSWORD ?? '', signal: controller.signal, + ...(process.env.WEBDAV_MAX_BYTES ? { maxBytes: Number(process.env.WEBDAV_MAX_BYTES) } : {}), + }).then(result => console.log(JSON.stringify(result, null, 2))).catch(() => { + // Network exceptions may contain destination URLs; keep credentials and URLs out of logs. + console.error(controller.signal.aborted ? '备份上传已取消' : '备份上传失败,请检查备份校验、远端权限、配额、同名文件及 WebDAV 日志。'); + process.exitCode = 1; + }); +} diff --git a/server/src/maintenance/webdav.ts b/server/src/maintenance/webdav.ts new file mode 100644 index 0000000..788d442 --- /dev/null +++ b/server/src/maintenance/webdav.ts @@ -0,0 +1,105 @@ +import { createHash, randomUUID } from 'node:crypto'; +import { createReadStream, createWriteStream } from 'node:fs'; +import { mkdtemp, readFile, rm, stat } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { isAbsolute, join, relative, resolve } from 'node:path'; +import { pipeline } from 'node:stream/promises'; +import JSZip from 'jszip'; +import { verifyBackup } from './backup.ts'; + +export interface WebDavOptions { + url: string; + username: string; + password: string; + name: string; + maxBytes?: number; + signal?: AbortSignal; +} + +async function hashFile(path: string): Promise { + const hash = createHash('sha256'); + for await (const chunk of createReadStream(path)) hash.update(chunk); + return hash.digest('hex'); +} + +/** Publish an immutable, verified backup ZIP to an existing WebDAV collection. */ +export async function uploadBackup(directory: string, options: WebDavOptions) { + const base = new URL(options.url); + if (base.protocol !== 'https:' && !(base.protocol === 'http:' && ['127.0.0.1', '[::1]', 'localhost'].includes(base.hostname))) { + throw new Error('WebDAV 必须使用 HTTPS(本机测试可用 HTTP)'); + } + if (base.username || base.password || base.search || base.hash) throw new Error('WebDAV URL 不得包含凭据、查询或片段'); + if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,119}\.zip$/.test(options.name)) throw new Error('备份名称必须是安全的 .zip 文件名'); + if (options.username.includes(':') || !options.username || !options.password) throw new Error('需要有效的 WebDAV 账号和密码'); + if (!base.pathname.endsWith('/')) base.pathname += '/'; + const limit = options.maxBytes ?? 2 * 1024 ** 3; + if (!Number.isSafeInteger(limit) || limit < 1 || limit > 4 * 1024 ** 3 - 1) throw new Error('备份上限必须介于 1 字节与 4 GiB 之间'); + const manifest = await verifyBackup(directory); + if (manifest.entries.reduce((sum, entry) => sum + (entry.size ?? 0), 0) > limit) throw new Error('备份超过配置大小上限'); + if (manifest.entries.length > 60000) throw new Error('备份文件过多,超出 ZIP32 支持范围'); + const scratch = await mkdtemp(join(tmpdir(), 'reader-webdav-')); + const archive = join(scratch, 'backup.zip'); + const destination = new URL(options.name, base), temporary = new URL('.reader-upload-' + randomUUID() + '.zip', base); + const authorization = 'Basic ' + Buffer.from(options.username + ':' + options.password).toString('base64'); + const request = (url: URL, init: RequestInit = {}) => fetch(url, { + ...init, redirect: 'manual', headers: { authorization, ...init.headers }, + signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(120_000)]) : AbortSignal.timeout(120_000), + }); + let staged = false; + try { + const zip = new JSZip(); + for (const entry of manifest.entries) { + options.signal?.throwIfAborted(); + if (entry.kind === 'directory') zip.folder(entry.path); + else if (entry.kind === 'link') zip.file(entry.path, entry.target!, { unixPermissions: 0o120777 }); + else zip.file(entry.path, createReadStream(join(directory, entry.path)), { unixPermissions: (await stat(join(directory, entry.path))).mode }); + } + zip.file('reader-backup.json', await readFile(join(directory, 'reader-backup.json')), { unixPermissions: 0o100600 }); + await pipeline(zip.generateNodeStream({ streamFiles: true, platform: 'UNIX', compression: 'STORE' }), createWriteStream(archive, { flags: 'wx', mode: 0o600 }), ...(options.signal ? [{ signal: options.signal }] : [])); + await verifyBackup(directory); + const size = (await stat(archive)).size; + if (size > limit) throw new Error('打包后超过配置大小上限'); + const sha256 = await hashFile(archive); + const existing = await request(destination, { method: 'HEAD' }); + await existing.body?.cancel(); + if (existing.status !== 404) throw new Error(existing.ok ? '远端同名备份已存在,不会覆盖' : `检查远端失败 HTTP ${existing.status}`); + for (let attempt = 0; attempt < 3; attempt++) { + options.signal?.throwIfAborted(); + staged = true; + const input = createReadStream(archive); + let response: Response; + try { + response = await request(temporary, { method: 'PUT', body: input, duplex: 'half', headers: { 'content-type': 'application/zip', 'content-length': String(size) } } as RequestInit); + } catch { + if (options.signal?.aborted) options.signal.throwIfAborted(); + if (attempt < 2) continue; + throw new Error('WebDAV 上传连接失败,临时文件将尝试清理'); + } finally { input.destroy(); } + await response.body?.cancel(); + if (response.ok) break; + if (attempt === 2 || ![408, 429, 500, 502, 503, 504].includes(response.status)) throw new Error(`WebDAV 上传失败 HTTP ${response.status}`); + } + const uploaded = await request(temporary); + if (!uploaded.ok || !uploaded.body) { await uploaded.body?.cancel(); throw new Error(`远端校验读取失败 HTTP ${uploaded.status}`); } + const remoteHash = createHash('sha256'); let bytes = 0; + for await (const chunk of uploaded.body) { + bytes += chunk.byteLength; + if (bytes > size) throw new Error('远端校验失败:长度不一致'); + remoteHash.update(chunk); + } + if (bytes !== size || remoteHash.digest('hex') !== sha256) throw new Error('远端校验失败:内容不一致'); + const moved = await request(temporary, { method: 'MOVE', headers: { destination: destination.href, overwrite: 'F' } }); + await moved.body?.cancel(); + if (moved.status !== 201) throw new Error(`WebDAV 发布失败 HTTP ${moved.status};请核对远端,未自动重试 MOVE`); + staged = false; + return { name: options.name, bytes: size, sha256 }; + } finally { + if (staged) { + await fetch(temporary, { method: 'DELETE', headers: { authorization }, redirect: 'manual', signal: AbortSignal.timeout(10_000) }) + .then(response => response.body?.cancel()).catch(() => {}); + } + const cleanupPath = relative(resolve(tmpdir()), resolve(scratch)); + if (!cleanupPath || isAbsolute(cleanupPath) || cleanupPath.startsWith('..')) throw new Error('Unsafe cleanup path'); + await rm(scratch, { recursive: true, force: true }); + } +} diff --git a/server/src/publications/chapter-quality.ts b/server/src/publications/chapter-quality.ts new file mode 100644 index 0000000..ef5d0ec --- /dev/null +++ b/server/src/publications/chapter-quality.ts @@ -0,0 +1,19 @@ +import sanitize from 'sanitize-html'; +import { badRequest } from '../lib/errors.ts'; +/** Counts readable Unicode characters, excluding whitespace; never treats markup as prose. */ +export function chapterQuality(body: string, mediaType: string): { characters: number; images: number; bytes: number } { + let text = body, images = 0; + if (mediaType.startsWith('text/html')) { + text = sanitize(body, { allowedTags: [], allowedAttributes: {} }); + text = text.replace(/&(?:nbsp|#160|#xA0);/gi, ' ').replace(/&(?:#x[0-9a-f]+|#\d+|[a-z]+);/gi, 'x'); + // Images were already validated and embedded by richContent. + images = (body.match(/]*src="data:image\//g) ?? []).length; + } + const characters = Array.from(text.replace(/[\s\u200b-\u200f\u202a-\u202e\u2060\ufeff]/g, '')).length; + return { characters, images, bytes: Buffer.byteLength(body,'utf8') }; +} +export function requireChapterContent(body: string, mediaType: string): ReturnType { + const quality = chapterQuality(body,mediaType); + if (!quality.characters && !quality.images) throw badRequest('此章节没有可读正文,请选择其他章节或书源', 'EMPTY_CHAPTER'); + return quality; +} diff --git a/server/src/publications/chapters.ts b/server/src/publications/chapters.ts index 8d11171..894c079 100644 --- a/server/src/publications/chapters.ts +++ b/server/src/publications/chapters.ts @@ -1,3 +1,4 @@ +import { requireChapterContent } from './chapter-quality.ts'; import { createHash, randomUUID } from 'node:crypto'; import type { Db } from '../db/index.ts'; import type { AssetPayload, Manifest } from '../indexer/formats/registry.ts'; @@ -168,6 +169,7 @@ export class ChapterPublications { return this.loader.resource(userId, publication.source_id, publication.publication_ref, imageRef, signal); }); } + requireChapterContent(body, resource.media_type); signal?.throwIfAborted(); // The provider protocol cannot request an old version. If a refresh won // while this request was in flight, never cache new bytes as old content. @@ -181,6 +183,21 @@ export class ChapterPublications { }); } + async inspect(provider: SourceProvider, context: SourceContext, publicationRef: string, chapterId: string) { + if (!provider.getManifest || !provider.readResource) throw badRequest('source has no chapter content'); + const started = performance.now(); + const snapshot = this.normalize(await provider.getManifest(context,publicationRef),publicationRef); + const chapter = snapshot.items.find(item => item.id === chapterId); + if (!chapter) throw notFound('所选章节已不存在,请重新获取目录', 'CHAPTER_GONE'); + const response = await provider.readResource(context,{publicationRef,ref:chapter.ref}); + const media = chapterMedia(chapter.mediaType); + if (chapterMedia(response.mediaType) !== media) { response.stream?.destroy(); throw badRequest('Invalid chapter media type'); } + let body = this.readBody(response,context.signal); + if (media.startsWith('text/html')) body = await richContent(body,ref => provider.readResource!(context,{publicationRef,ref})); + context.signal.throwIfAborted(); + return { chapterId, title:chapter.title, latestChapter:snapshot.items.at(-1)?.title ?? '', ...requireChapterContent(body,media), elapsedMs:Math.round(performance.now()-started) }; + } + binding(userId: string, bookId: string) { return this.owned(userId, bookId); } switchSource(provider: SourceProvider, context: SourceContext, bookId: string, entryRef: string, publicationRef: string, chapterId: string, expectedRevision: string) { @@ -205,6 +222,7 @@ export class ChapterPublications { let body = this.readBody(response, context.signal); if (chapterMedia(chapter.mediaType).startsWith('text/html')) body = await richContent(body, imageRef => provider.readResource!(context, { publicationRef, ref: imageRef })); + requireChapterContent(body, chapterMedia(chapter.mediaType)); context.signal.throwIfAborted(); const snapshotJson = JSON.stringify(snapshot), revision = hash(snapshotJson), now = Date.now(); this.db.transaction(() => { diff --git a/server/src/services/opds.ts b/server/src/services/opds.ts new file mode 100644 index 0000000..75c30a4 --- /dev/null +++ b/server/src/services/opds.ts @@ -0,0 +1,45 @@ +import { createHash, randomBytes, randomUUID } from 'node:crypto'; +import type { Db } from '../db/index.ts'; +import { badRequest, unauthorized } from '../lib/errors.ts'; + +const digest = (value: string) => createHash('sha256').update(value).digest('hex'); + +/** Read-only application passwords, deliberately separate from Reader sessions. */ +export class OpdsCredentials { + constructor(private readonly db: Db) {} + + list(userId: string) { + return this.db.all<{ id: string; name: string; createdAt: number; expiresAt: number }>( + 'SELECT id,name,created_at AS createdAt,expires_at AS expiresAt FROM opds_credentials WHERE user_id=? ORDER BY created_at DESC', userId, + ); + } + + create(userId: string, name: unknown) { + if (typeof name !== 'string' || !name.trim() || name.length > 80) throw badRequest('请输入不超过 80 字的客户端名称'); + if (this.list(userId).length >= 20) throw badRequest('最多保存 20 个 OPDS 客户端,请先撤销不用的凭据'); + const id = randomUUID(), password = randomBytes(32).toString('base64url'); + const createdAt = Date.now(), expiresAt = createdAt + 365 * 86400_000; + this.db.run('INSERT INTO opds_credentials(id,user_id,name,token_hash,created_at,expires_at) VALUES(?,?,?,?,?,?)', + id, userId, name.trim(), digest(password), createdAt, expiresAt); + return { id, name: name.trim(), username: id, password, createdAt, expiresAt }; + } + + revoke(userId: string, id: string): void { + this.db.run('DELETE FROM opds_credentials WHERE user_id=? AND id=?', userId, id); + } + + authenticate(header: string | undefined): string { + if (!header?.startsWith('Basic ') || header.length > 512) throw unauthorized('OPDS client credentials required', 'OPDS_AUTH_REQUIRED'); + const value = Buffer.from(header.slice(6), 'base64').toString('utf8'), split = value.indexOf(':'); + const row = this.db.get<{ user_id: string }>( + `SELECT c.user_id FROM opds_credentials c JOIN users u ON u.id=c.user_id + WHERE c.id=? AND c.token_hash=? AND c.expires_at>? AND u.disabled=0`, + value.slice(0, split), digest(value.slice(split + 1)), Date.now(), + ); + if (split < 1 || !row) throw unauthorized('OPDS client credentials invalid or expired', 'OPDS_AUTH_REQUIRED'); + return row.user_id; + } +} + +export const xml = (value: string | number) => String(value).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, '') + .replace(/[&<>"']/g, character => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character]!); diff --git a/server/src/services/reading-overrides.ts b/server/src/services/reading-overrides.ts new file mode 100644 index 0000000..7040061 --- /dev/null +++ b/server/src/services/reading-overrides.ts @@ -0,0 +1,33 @@ +import type { Db } from '../db/index.ts'; +import { badRequest, conflict } from '../lib/errors.ts'; + +export interface ReadingOverrides { version: number; corrections: Array<{ id: string; anchor: { sectionId: string; start: number; end: number; quote: string; prefix: string; suffix: string }; replacement: string }>; headingPrefix: string } +const empty = (): ReadingOverrides => ({ version: 0, corrections: [], headingPrefix: '' }); +/** Personal, versioned overlays live beside progress in DATA_DIR; source files are never changed. */ +export class ReadingOverrideService { + constructor(private readonly db: Db) {} + get(userId: string, bookId: string): ReadingOverrides { + const row = this.db.get<{ payload: string; version: number }>('SELECT payload,version FROM reading_overrides WHERE user_id=? AND book_id=? ORDER BY version DESC LIMIT 1',userId,bookId); + return row ? { ...JSON.parse(row.payload) as ReadingOverrides, version: row.version } : empty(); + } + save(userId: string, bookId: string, input: ReadingOverrides): ReadingOverrides { + if (!input || !Number.isSafeInteger(input.version) || typeof input.headingPrefix !== 'string' || input.headingPrefix.length > 60 || /[\r\n]/.test(input.headingPrefix) || !Array.isArray(input.corrections) || input.corrections.length > 500) throw badRequest('invalid reading overrides'); + const ids = new Set(); + for (const correction of input.corrections) { + const a = correction?.anchor; + if (!correction || typeof correction.id !== 'string' || !correction.id || correction.id.length > 100 || ids.has(correction.id) || typeof correction.replacement !== 'string' || correction.replacement.length > 4000 || !a || typeof a.sectionId !== 'string' || a.sectionId.length > 4096 || typeof a.quote !== 'string' || !a.quote || a.quote.length > 4000 || typeof a.prefix !== 'string' || a.prefix.length > 32 || typeof a.suffix !== 'string' || a.suffix.length > 32 || !Number.isSafeInteger(a.start) || a.start < 0 || !Number.isSafeInteger(a.end) || a.end <= a.start || a.end - a.start !== a.quote.length) throw badRequest('invalid correction'); + ids.add(correction.id); + } + return this.db.transaction(() => { + const previous = this.get(userId,bookId); + if (input.version !== previous.version) throw conflict('整理规则已被另一设备修改,请重新打开后重试'); + const next = { version: previous.version + 1, headingPrefix: input.headingPrefix, corrections: input.corrections }; + this.db.run('INSERT INTO reading_overrides(user_id,book_id,version,payload) VALUES(?,?,?,?)',userId,bookId,next.version,JSON.stringify(next)); + return next; + }); + } + undo(userId: string, bookId: string, version: number): ReadingOverrides { + const row = this.db.get<{ payload: string }>('SELECT payload FROM reading_overrides WHERE user_id=? AND book_id=? AND version = library ? [] : [userId]; + if (options.fileFormats) { + where.push(`b.format IN (${options.fileFormats.map(() => '?').join(',') || 'NULL'})`); + params.push(...options.fileFormats); + } if (options.search) { where.push('(b.title LIKE ? OR b.author LIKE ? OR b.series LIKE ? OR b.isbn = ?)'); diff --git a/server/src/services/source-access.ts b/server/src/services/source-access.ts new file mode 100644 index 0000000..218cf1e --- /dev/null +++ b/server/src/services/source-access.ts @@ -0,0 +1,43 @@ +import type { Db } from '../db/index.ts'; + +export type AccessState = 'unknown' | 'reachable' | 'auth-required' | 'verification-required'; + +/** Last observed access, never a claim that a website session is still logged in. */ +export class SourceAccess { + constructor(private readonly db: Db) {} + + version(sourceId: string, userId: string): number { + return this.db.get<{ generation: number }>( + 'SELECT generation FROM source_access WHERE source_id=? AND user_id=?', sourceId, userId, + )?.generation ?? 0; + } + + reset(sourceId: string, userId?: string): void { + if (userId === undefined) { + this.db.run("UPDATE source_access SET generation=generation+1,state='unknown',checked_at=NULL WHERE source_id=?", sourceId); + return; + } + this.db.run( + `INSERT INTO source_access(source_id,user_id,generation,state) VALUES(?,?,1,'unknown') + ON CONFLICT(source_id,user_id) DO UPDATE SET generation=generation+1,state='unknown',checked_at=NULL`, + sourceId, userId, + ); + } + + record(sourceId: string, userId: string, generation: number, state: AccessState): void { + if (this.version(sourceId, userId) !== generation) return; + this.db.run( + `INSERT INTO source_access(source_id,user_id,generation,state,checked_at) VALUES(?,?,?,?,?) + ON CONFLICT(source_id,user_id) DO UPDATE SET state=excluded.state,checked_at=excluded.checked_at + WHERE source_access.generation=excluded.generation`, + sourceId, userId, generation, state, Date.now(), + ); + } + + get(sourceId: string, userId: string) { + const row = this.db.get<{ state: AccessState; checked_at: number | null }>( + 'SELECT state,checked_at FROM source_access WHERE source_id=? AND user_id=?', sourceId, userId, + ); + return { state: row?.state ?? 'unknown', checkedAt: row?.checked_at ?? null }; + } +} diff --git a/server/src/services/source-host.ts b/server/src/services/source-host.ts index 1936c67..f06cc14 100644 --- a/server/src/services/source-host.ts +++ b/server/src/services/source-host.ts @@ -1,3 +1,4 @@ +import { SourceAccess } from './source-access.ts'; import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto'; import type { FastifyBaseLogger } from 'fastify'; import type { AppConfig } from '../config/index.ts'; @@ -22,12 +23,14 @@ export class SourceHost { readonly plugins: PluginManager; readonly chapters: ChapterPublications; readonly updates: ChapterUpdates; + private readonly access: SourceAccess; private readonly key: Buffer; private readonly acquisitions: FileAcquisitions; private activeCalls = 0; private readonly activeSources = new Map(); constructor(private readonly db: Db, private readonly config: AppConfig, shelf: () => ShelfService, private readonly log?: FastifyBaseLogger) { + this.access = new SourceAccess(db); this.key = createHash('sha256').update('reader/source-credentials/v1\0').update(config.jwtSecret).digest(); this.acquisitions = new FileAcquisitions(db, config); this.chapters = new ChapterPublications(db, { @@ -112,7 +115,7 @@ export class SourceHost { this.db.run('UPDATE source_instances SET name = ?, config_json = ?, enabled = ? WHERE id = ?', name.trim(), configuration, patch.enabled === undefined ? row.enabled : Number(patch.enabled), id); // Changing a destination must not forward existing users' secrets to it. - if (configuration !== row.config_json) this.db.run('DELETE FROM source_credentials WHERE source_id = ?', id); + if (configuration !== row.config_json) { this.db.run('DELETE FROM source_credentials WHERE source_id = ?', id); this.access.reset(id); } }); } @@ -126,14 +129,28 @@ export class SourceHost { this.db.run('DELETE FROM source_instances WHERE id = ?', id); } + credentialStatus(sourceId: string, userId: string) { + const row = this.instance(sourceId), descriptor = this.registry.get(row.plugin_id,row.source_type)?.provider.descriptor; + const saved = new Set(this.db.all<{key:string}>('SELECT key FROM source_credentials WHERE source_id=? AND user_id=?',sourceId,userId).map(item=>item.key)); + return { ...this.access.get(sourceId,userId), available: !!row.enabled && !!descriptor, + fields: (descriptor?.credentialKeys ?? []).map(field=>({key:field.key,label:field.label,configured:saved.has(field.key)})) }; + } + deleteCredential(sourceId: string, userId: string, name: string): void { + this.instance(sourceId); + this.db.transaction(()=>{ this.db.run('DELETE FROM source_credentials WHERE source_id=? AND user_id=? AND key=?',sourceId,userId,name); this.access.reset(sourceId,userId); }); + } setCredential(sourceId: string, userId: string, name: string, value: string): void { if (!/^[a-z][a-z0-9_.-]{0,63}$/.test(name) || value.length > 16_384) throw badRequest('invalid credential'); this.instance(sourceId); - this.db.run( - `INSERT INTO source_credentials (source_id, user_id, key, encrypted_value) VALUES (?, ?, ?, ?) + if (!value) { this.deleteCredential(sourceId,userId,name); return; } + this.db.transaction(() => { + this.db.run( + `INSERT INTO source_credentials (source_id, user_id, key, encrypted_value) VALUES (?, ?, ?, ?) ON CONFLICT(source_id, user_id, key) DO UPDATE SET encrypted_value = excluded.encrypted_value`, - sourceId, userId, name, this.encrypt(value), - ); + sourceId, userId, name, this.encrypt(value), + ); + this.access.reset(sourceId, userId); + }); } async browse(userId: string, id: string, request: { ref?: string; cursor?: string; limit?: number }, signal?: AbortSignal) { @@ -172,9 +189,18 @@ export class SourceHost { const acquisition = await provider.acquire(ctx, { entryRef }); if (acquisition.kind !== 'chapters' || !provider.getManifest) throw badRequest('alternative has no chapters'); const snapshot = await provider.getManifest(ctx, acquisition.publicationRef); - return { chapters: snapshot.items.map(item => ({ id: item.id, title: item.title })) }; + return { chapters: snapshot.items.map(item => ({ id: item.id, title: item.title })), latestChapter: [...snapshot.items].sort((a,b)=>a.seq-b.seq).at(-1)?.title ?? '' }; }, signal); } + async switchQuality(userId: string, bookId: string, entryRef: string, chapterId: string, signal?: AbortSignal) { + const binding = this.chapters.binding(userId,bookId); + return this.call(userId,binding.source_id,async (provider,ctx) => { + if (!provider.alternatives) throw badRequest('source does not support alternatives'); + const acquisition = await provider.acquire(ctx,{entryRef}); + if (acquisition.kind !== 'chapters') throw badRequest('alternative has no chapters'); + return this.chapters.inspect(provider,ctx,acquisition.publicationRef,chapterId); + },signal); + } async switchSource(userId: string, bookId: string, entryRef: string, chapterId: string, revision: string, signal?: AbortSignal) { const binding = this.chapters.binding(userId, bookId); return this.call(userId, binding.source_id, async (provider, ctx) => { @@ -229,6 +255,7 @@ export class SourceHost { if (!row.enabled) throw badRequest('source is disabled', 'SOURCE_DISABLED'); const registration = this.registry.get(row.plugin_id, row.source_type); if (!registration) throw notFound('source plugin is not installed', 'PLUGIN_UNAVAILABLE'); + const accessVersion = this.access.version(id,userId); const active = this.activeSources.get(id) ?? 0; if (active >= 2 || this.activeCalls >= 8) { throw new AppError(429, 'RATE_LIMITED', 'source is busy; retry after current requests complete'); @@ -240,8 +267,16 @@ export class SourceHost { const signal = externalSignal ? AbortSignal.any([externalSignal, controller.signal]) : controller.signal; try { signal.throwIfAborted(); - return await action(registration.provider, this.context(row, userId, signal)); + const result = await action(registration.provider, this.context(row, userId, signal)); + if (userId && !signal.aborted) { + const errors = (result as {errors?:Array<{code?:string}>} | null)?.errors; + const state = errors?.some(e=>e.code === 'VERIFICATION_REQUIRED') ? 'verification-required' : errors?.some(e=>['AUTH_REQUIRED','AUTH_EXPIRED'].includes(e.code ?? '')) ? 'auth-required' : errors?.length ? null : 'reachable'; + if (state) this.access.record(id,userId,accessVersion,state); + } + return result; } catch (error) { + const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : ''; + if (userId && !signal.aborted && ['AUTH_REQUIRED','AUTH_EXPIRED','VERIFICATION_REQUIRED'].includes(code)) this.access.record(id,userId,accessVersion,code === 'VERIFICATION_REQUIRED' ? 'verification-required' : 'auth-required'); if (controller.signal.aborted) throw new AppError(504, 'SOURCE_TIMEOUT', 'source request exceeded its execution deadline'); if (externalSignal?.aborted) throw new AppError(499, 'SOURCE_CANCELLED', 'source request cancelled'); throw error; @@ -278,7 +313,7 @@ export class SourceHost { return row ? this.decrypt(row.encrypted_value) : undefined; }, set: async (key, value) => this.setCredential(sourceId, userId, key, value), - delete: async (key) => this.db.run('DELETE FROM source_credentials WHERE source_id = ? AND user_id = ? AND key = ?', sourceId, userId, key), + delete: async (key) => this.deleteCredential(sourceId,userId,key), }; } private storage(sourceId: string, userId: string): SourceStorage { diff --git a/server/src/services/sync.ts b/server/src/services/sync.ts index 4d4a018..179c52a 100644 --- a/server/src/services/sync.ts +++ b/server/src/services/sync.ts @@ -99,6 +99,8 @@ export class SyncService { rejected += 1; continue; } + const owner = this.db.get<{user_id:string;book_id:string}>('SELECT user_id,book_id FROM notes WHERE id=?',item.id); + if (owner && (owner.user_id !== userId || owner.book_id !== item.bookId)) { rejected++; continue; } this.upsertNote(userId, item); accepted += 1; } @@ -131,12 +133,14 @@ export class SyncService { `INSERT INTO notes (id, user_id, book_id, type, locator, text, comment, color, updated_at, deleted) VALUES (?,?,?,?,?,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET + type = CASE WHEN excluded.updated_at >= notes.updated_at THEN excluded.type ELSE notes.type END, locator = CASE WHEN excluded.updated_at >= notes.updated_at THEN excluded.locator ELSE notes.locator END, text = CASE WHEN excluded.updated_at >= notes.updated_at THEN excluded.text ELSE notes.text END, comment = CASE WHEN excluded.updated_at >= notes.updated_at THEN excluded.comment ELSE notes.comment END, color = CASE WHEN excluded.updated_at >= notes.updated_at THEN excluded.color ELSE notes.color END, deleted = CASE WHEN excluded.updated_at >= notes.updated_at THEN excluded.deleted ELSE notes.deleted END, - updated_at = MAX(excluded.updated_at, notes.updated_at)`, + updated_at = MAX(excluded.updated_at, notes.updated_at) + WHERE notes.user_id = excluded.user_id AND notes.book_id = excluded.book_id`, record.id, userId, record.bookId, record.type ?? 'note', record.locator ?? '', record.text ?? '', record.comment ?? '', record.color ?? '', record.updatedAt ?? Date.now(), record.deleted ? 1 : 0, diff --git a/server/src/services/tts.ts b/server/src/services/tts.ts index b2e553f..2ef01b5 100644 --- a/server/src/services/tts.ts +++ b/server/src/services/tts.ts @@ -149,33 +149,24 @@ export class TtsService { const target = this.buildUrl({ text, voice, speed, ...(input.format ? { format: input.format } : {}) }); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), config.timeoutMs); - let response: Response; + let bytes: Uint8Array, contentType: string; try { - response = await fetch(target, { - method: 'GET', - headers: { - accept: 'audio/*', - ...(config.token ? { authorization: `Bearer ${config.token}` } : {}), - }, + const response = await fetch(target, { + method: 'GET', headers: { accept: 'audio/*', ...(config.token ? { authorization: 'Bearer ' + config.token } : {}) }, signal: controller.signal, }); - } catch (err) { - const message = err instanceof Error && err.name === 'AbortError' ? 'timed out' : 'unreachable'; - throw badRequest(`HTTP TTS service ${message}`, 'TTS_UPSTREAM'); - } finally { - clearTimeout(timer); - } - if (!response.ok) { - throw badRequest(`HTTP TTS service answered ${response.status}`, 'TTS_UPSTREAM'); - } - const contentType = (response.headers.get('content-type') ?? 'audio/mpeg').split(';')[0]!.trim(); - // A service that answers with HTML is a misconfiguration (a wrong URL landing - // on a web page), and streaming that into an `