书源聚合阅读器:兼容 Legado 书源规则格式的解析引擎 + 自带阅读器。整套跑在 Cloudflare 上 (Workers + D1 + 静态资源),也能跑在自己的机器上 —— 包括 Docker。
主流阅读器都上不了 Workers:Legado 是安卓 App、hectorqin/reader 是 JVM 且带状态、 any-reader 用的是另一套源格式。而书源规则本身是声明式的,所以这里复刻的是一套规则语法, 不是某个应用的内部实现。
本项目不提供、也不分发任何书源。第一次进来是空的,要在「书源」页导入一份书源 JSON 才有 东西可搜 —— 见下面「书源」。
逐项的兼容性账本(每一行都跟着对应的轮次)在 EXPERIENCE.md 的「当前状态」, 下面是按用途归的一层。
覆盖的是「能真的读完一本书」那条路径,而不是某个子集:
| 一类 | 说明 |
|---|---|
| 四种方言 | JSOUP 默认规则(class.x.0@tag.a@href)、@css:、XPath(完整 XPath 1.0)、JSONPath(含简单过滤器)与 JSON 的裸字段名 |
| 取值链 | ##正则##替换、### OnlyOne、AllInOne;&& / || / %% 连接符(不切进 JS 代码、引号与 JSONPath 过滤器里) |
| 变量 | @put: / @get:(含跨请求那一路)与 ruleBookInfo.init(铺变量与换根两种写法) |
| 脚本 | @js: 与 <js></js>,写在 URL 字段、字段规则、列表规则里都认;result 按脚本自己的写法绑成节点或字符串 |
| 列表规则 | @js: 返回数组、<js> 块 + 尾段选择器(条目保留节点)、CSS 式首段 + @ 步骤与下标后缀、开头的 + 标记 |
| 四类源 | 文本 / 音频 / 图片 / 文件(bookSourceType 0–3),媒体一律由服务端代取(防盗链、混合内容、跨域) |
| 翻页 | nextTocUrl(目录最多 20 页)与 nextContentUrl(跨页章节最多 10 页) |
| 编码 | GBK / GB2312 等非 UTF-8 站点自动嗅探 + 按站点声明解码 |
规则里的 JS 跑在 QuickJS(asyncify 版)里,java.*、org.jsoup.Jsoup、Packages.java.*
按 Legado 的签名对齐。缺的东西显式报错并报出名字,不静默返回空 —— 后者会让书源表现成
「搜不到书」,排查成本极高。
- 取网:
java.ajax/get/post/ajaxAll,在脚本里当同步函数用;重定向自己跟,每一跳的Set-Cookie都收 - 纯计算:AES / DES、MD5 / SHA-256、base64 / hex、
java.getString/getElements/timeFormat - 全局对象:
source/book/chapter/cookie(cookie 罐)/cache/infoMap/Packages - 书源自带的
jsLib作为前置脚本先跑一遍
零依赖、无构建步骤:public/ 直接作为静态资源发布,模块用原生 ESM 分文件。
- 书架 / 搜索 / 发现 / 书源管理 / 详情 / 阅读 / 账号
- 搜索:结果按书合并(同一本在多个书源上只占一条,点开可以挑读哪个源);一次能搜多少书源由部署决定 —— 线上那份按页(免费计划每请求 10 毫秒 CPU),自建与容器一次搜完(见
DOCKER.md) - 换源:读到一半发现这个源的错字多 / 缺章,可以换到别的源接着读。书架记录与阅读位置一起搬,落点按章名对;对不上会如实说「从第一章开始」,而不是假装对上了
- 阅读:滚动与翻页两种模式,翻页走 CSS 多栏排版(换字号、换窗口宽都不用重新分页);手势与键盘走同一套翻页;进度记到页
- 显示设置:背景色 8 种 + 跟随主题、字号、行距、翻页动画、主题、强调色;字号有四个入口(面板、双指捏合、
Ctrl+滚轮、键盘) - 章内搜索、书签(可带备注)、笔记、替换净化(浏览器端、渲染前应用、实时预览)
- PWA:装到桌面、断网还能开(缓存界面与正在读的那本书)
- 账号:书架 / 进度 / 书签 / 笔记 / 替换规则跟着账号走;书源是全局的
界面上的错误一律就地显示原因,而不是只给一句「出错了」。
- 导入 Legado 书源 JSON(裸数组或
{"sources":[...]}),多文件与拖放都行;逐条校验、逐条报拒绝原因 - 同一个站点地址重复导入算更新:只刷规则,保留改过的名字、分组与启用状态
- 启停单条或一批;按分组浏览、按名字 / 分组 / 状态 / 能力(搜索 / 发现 / 登录)筛
- 登录态:跑一次
loginUrl落库,之后每趟请求都带上;写了loginUi的源会在界面上渲染成表单 - 列表接口
/api/sources带ETag,没变回 304
npm install
npm run db:migrate # 建本地 D1 表(首次、以及每次新增迁移后)
npm run dev # 打开 http://127.0.0.1:8787打开就是登录页 —— 整个应用需要登录,先注册一个账号。然后去「书源」页导入一份书源 JSON; 在那之前书架与搜索都是空的,这是对的。
npm test # 单元测试(1135 项,Node 里秒级跑完)
npm run smoke # 端到端冒烟(要另开终端,且本地服务开着;它只打真实 HTTP)
npm run typecheck # 类型检查
npm run format:check # 格式检查| 方式 | 适合 | 入口 |
|---|---|---|
| Cloudflare 两份 | 想要一个公网地址,不想自己维护进程 | npm run deploy:all —— 页面发 Pages、接口发 Worker,两次独立部署;首次要建 D1 并跑远端迁移,步骤见 EXPERIENCE.md 的「部署」 |
| 自建 Node | 不想依赖 Cloudflare,或者嫌免费计划每请求 10 毫秒的 CPU 太紧 | npm run start:node(本机 SQLite,没有 CPU 上限) |
| Docker | 想把依赖、Node 版本、数据目录封在一起 | docker compose up -d --build —— 也是两份(页面 nginx + 接口 node),见 DOCKER.md |
Cloudflare 上那两份是页面(Pages:public/ 的静态资源 + 一层把 /api 接回去的同源反代)
与接口(Worker:src/index.ts + D1)。分开部署的好处是改界面不必碰接口、改接口不必重发
页面;代价是两者之间多了一层反代(见 EXPERIENCE.md 里那一节)。
本项目不提供、也不分发任何书源。规则引擎是中立的工具,指向哪个站点、抓取什么内容, 由使用者自行决定并自负其责 —— Legado、any-reader 这些项目也都是这么划线的。
请遵守目标站点的服务条款与 robots 协议,并注意所在地区的版权法规。
项目自带的「内置测试站点」只包含项目自己生成的内容,用于验证链路;它由 ENABLE_FIXTURE
控制,线上默认关闭。
Dockerfile / Dockerfile.page 容器那两份:接口(node)与页面(nginx)
docker-compose.yml / page.nginx.conf 两个容器怎么拼起来、页面那层反代(用法见 DOCKER.md)
DOCKER.md 容器部署:起法与参数、数据与备份、与 Cloudflare 那份的差异
pages/ Cloudflare Pages 的两个约定文件(_worker.js / _routes.json)
EXPERIENCE.md 实现细节与踩坑记录:能力总表、规则语义、沙箱、每轮改了什么、验证、接口
migrations/ D1 迁移(按序号递增,不可回改已应用的)
public/ 前端(零依赖、无构建步骤)
src/ 服务端:路由 / 规则引擎 / 数据层 / 平台适配(逐文件说明见 EXPERIENCE.md)
test/ 单元测试(快的那些;全量扫描默认跳过)
scripts/ 冒烟、体检与探针
| 文档 | 里面有什么 |
|---|---|
| EXPERIENCE.md | 工程经验与实现笔记。规则引擎每一处的语义与取舍、沙箱与全局对象、每一轮改了什么 / 为什么 / 怎么验、验证命令、接口清单 |
| DOCKER.md | 容器部署。起法与参数、数据放在哪与怎么备份、与 Cloudflare 那份的差异、实测记录 |
| TODO.md | 待办。每条写清「为什么 / 已经到哪一步 / 下一步做什么 / 怎么算做完」 |
src/changelog.ts |
版本更新记录,/api/version 与界面「关于」页读它 |
AGPL-3.0-or-later。LICENSE 是 AGPL-3.0 的标准原文,与 SPlayer 项目里那份逐字相同
(git hash-object LICENSE 两边都是 dbbe3558157f5861bff35dcb37b328b679b0ccfd)。
package.json 里的 SPDX 标识写的是 AGPL-3.0-or-later 而不是 -only:前者允许被许可方
选用后续版本,两者法律含义不同。
选 AGPL 而不是 MIT,是因为这是个网络服务:AGPL 第 13 条要求「用户通过网络与这个程序交互时 必须能拿到它的源代码」,这一条正好挡住「拿去做闭源 SaaS」。
部署时要做到这一条:前端页脚里有一个指向
https://github.com/hashiqi12138/reader-cloudflare 的「源代码」链接,而 npm run smoke
会断言它存在 —— 删掉 CI 就会红(许可义务靠「记得」是保不住的)。改仓库名或换地址时,
记得同步改 public/index.html 页脚里那一个链接:链接失效等于没履行。
AGPL 只约束本项目的代码,不改变上面「书源」那一条。