hectorqin
|
a131a8cc45
|
feat(plugins): add bounded diagnostic output
|
2026-09-22 15:56:22 +08:00 |
|
hectorqin
|
5f8554eadd
|
feat(search): stream source results over HTTP
|
2026-09-22 14:38:36 +08:00 |
|
hectorqin
|
171297dc39
|
feat(search): add rolling multi-source sessions
|
2026-09-22 13:33:44 +08:00 |
|
hectorqin
|
b49caf0ffc
|
feat(sources): add cancellable batch search and configurable concurrency
|
2026-09-22 10:30:46 +08:00 |
|
hectorqin
|
3420bf07ce
|
fix(library): 限定管理员上传并完善文件管理导航与布局
|
2026-09-21 15:25:28 +08:00 |
|
hectorqin
|
8e8ca23d3b
|
fix(sources): 完善配置交互并增加真实端到端测试
|
2026-09-21 13:03:30 +08:00 |
|
hectorqin
|
c27c505f24
|
feat(sources): 按书源实例隔离插件配置并拆分管理 Tab
|
2026-09-21 12:28:50 +08:00 |
|
hectorqin
|
964a666f5b
|
feat(plugins): 支持独立配置页、多书源订阅与目录换源
|
2026-09-21 11:50:21 +08:00 |
|
hectorqin
|
5ecf918551
|
feat(plugins): support independently installed npm providers
|
2026-09-21 10:29:17 +08:00 |
|
hectorqin
|
c192fc2f12
|
feat(sources): add configuration pages and chapter updates
|
2026-09-20 23:59:12 +08:00 |
|
hectorqin
|
a6fddab953
|
feat(reader): 接入插件章节阅读与目录刷新
接入远程章节出版物、稳定定位、服务端与设备端缓存及手动刷新;补充账号隔离、同步并发保护、API 和设计文档。
|
2026-09-20 18:40:09 +08:00 |
|
hectorqin
|
037bdd6880
|
feat(sources): 增加内置 local、OPDS 与进程插件系统
|
2026-09-20 17:51:12 +08:00 |
|
hectorqin
|
31b5459a47
|
fix(web,server): 下架按下架的书 id 定位,书库两半各归其位
|
2026-09-20 12:32:43 +08:00 |
|
hectorqin
|
ae397b6181
|
feat(web,server): 补全书架的加入/移除两侧并统一可见性判据
|
2026-09-20 11:52:15 +08:00 |
|
hectorqin
|
558f44e206
|
feat(web,server): 书架默认按最近阅读排,书库拆成预览与文件两页
|
2026-09-19 14:17:55 +08:00 |
|
hectorqin
|
f2701da703
|
fix(web,server): 继续阅读带上完整书对象,书库能上架,放大镜回到中线
三处 #40 后续:继续阅读卡片点了报「不在书架上」、书库的书加不回书架、
书架搜索框的放大镜高了一行。
|
2026-09-19 11:46:42 +08:00 |
|
hectorqin
|
69993d7b37
|
Merge remote-tracking branch 'origin/main' into auto/reader-paragraphs-layout-images-eb50
|
2026-09-19 09:13:54 +08:00 |
|
hectorqin
|
0551f21e4e
|
fix(web,server): 段落不再合成一块,插图重新画出来,工具栏对齐
|
2026-09-19 09:09:12 +08:00 |
|
hectorqin
|
d489c45cf3
|
Merge remote-tracking branch 'origin/main' into auto/shelf-library-pagination-d4fc
|
2026-09-19 08:49:21 +08:00 |
|
hectorqin
|
c7d065bd52
|
feat(web): 书架与书库拆成两个可分页的屏幕
|
2026-09-19 08:45:37 +08:00 |
|
hectorqin
|
b8fda56c34
|
fix(web,server): 底部滑杆量章节内页数,快捷列随工具栏收放,chapter-full 改名
|
2026-09-19 08:17:46 +08:00 |
|
hectorqin
|
1c2d07ba4c
|
fix(web,server): 让服务端只交章节,并去掉成对的 sync 请求
服务端仍在做排版:`chapter-html:<n>` 自己断段、自己提升标题、自己吐
`<p>`。于是 reader 设置里的缩进和段间距只能作用于服务端已经渲染好的那一份,
流式那条路(`chapter:<n>`)拿不到段落,同一本书因为走哪条传输而看起来不一样。
客户端的 `retypePlainText` 于是把服务端断好的段落再拆一遍——同一个判断被两个
地方执行,两次的结果就会分歧。
`POST /api/v1/sync` 的响应本来就是合并后的状态(服务端在两个接口上返回同一份
`pull`),但客户端把它丢掉,然后又为同一份数据发一个 `GET`。读者每翻一页都发
一个 POST 加一个 GET,网络面板里成对出现。另外 `setState` 每次调用都广播,而
阅读页的状态监听会重绘整棵 chrome 树,一个同步循环通知三次。
现在服务端只把整章字符交出来(`text/plain`,`chapter-html:` 不截断),断段、
去段前空格、提升标题全部由 `web/src/formats/segments.ts` 一处决定;push 的返回
值直接喂给 `applyPulled`,空闲轮询才真的发 GET;`setState` 值未变就不广播。
「这一章是不是纯文本」改由 manifest 声明(item 的 `format: 'html'` →
`Section.plainText` → `isPlainText()`),不再靠 `.txt-body` 嗅探:服务端现在发
裸字符,嗅探对一章节全是纯文本的章节会答「不是」,读者会看到一本没有段落、
没有缩进、样式表也没挂上的小说。`.txt-body` 标记降级为旧服务端/旧缓存的兜底。
放弃的方案:保留服务端渲染、只在客户端覆盖样式。缩进和段间距是阅读偏好,服务端
渲染就等于把每次调整变成一次往返,而这个往返正是问题本身。也考虑过让
`chapter-html:` 直接返回服务端渲染的裸 `<p>`(不带 wrapper),但那仍然有两个
地方在判断「一段是什么」。
Ref: #34
|
2026-09-18 23:39:55 +08:00 |
|
hectorqin
|
7b9c786c83
|
feat(web): 纯文本排版改在客户端,并让顶栏底栏一起收放
现状问题。TXT 的段落和缩进原本由服务端决定:`chapter-html:<n>` 把章节渲染成
带 `<p>` 的片段,客户端只是把它塞进 shadow root。于是三件事同时不对。一是缩进
设定只对服务端已经渲染好的那份生效,windowed 路径拿到的 `chapter:<n>` 是纯字符
没有段落,同一个读者同一本书会因为走哪条传输而长得不一样。二是抓取来的文件里
用来当缩进的段前空格被原样留下,和读者的缩进叠加,恰好在最需要这个设定的文件
上看起来像没生效。三是这些设定是每台设备各自的阅读偏好,烧进响应就等于把每次
调整变成一次往返。另一件事是顶栏和底栏可以各自隐藏,读者收起导航后底栏仍压着
每一页的最后两行。
方案依据。把「一段是什么」收敛到客户端的唯一定义(`formats/segments.ts`),
本地加载、windowed、流式三条路都走它,服务端的职责缩小为交出完整章节和在原文
上断段。`ReaderView` 在渲染前对纯文本章节再过一遍这个拆分(按块读回、拼回空行
再拆,所以服务端断得对不对不影响观感),缩进、段间距、去掉段前空格因此无需请求
即可作用于两条路径。顶栏与前底栏由 `.reader-screen` 上的一个 `data-chrome` 统一
开关,收起时两条栏一起归零高度并滑出、正文长满整屏,同时右侧浮出一列快捷按钮,
否则收起工具栏会变成「必须离开才能操作」的状态。
放弃方案。把服务端的 `chapter-html` 直接删掉:`href` 同时是阅读位置的标识,且
`format: 'html'` 字段已是既有契约,删掉会让旧客户端与旧位置一起失效,所以保留
作为传输,只去掉它承担的排版职责。让缩进靠服务端属性(`data-indent`)继续驱动:
那正是「设定是响应的属性」这一问题的根源。收起态用两个独立开关:写法上就能只藏
一半,而这恰是读者报的那个 bug。
改动点。新增 `web/src/formats/segments.ts`(分段、去段前空格、HTML/节点两种形
式、跨 chunk 的 carry 判定);`reader-view.ts` 增加 `retypePlainText` 并去掉旧
排版表里读服务端属性的规则;`render/text.ts` 按 `carry` 拼接跨块段落;服务端
`text-html.ts` 去掉 `indent` 选项、不再截断;`reader-chrome.tsx` 增加侧边快捷列
与设置面板顶部的「快捷」一行;`reader.css` 增加两条栏的收放与按钮列样式;图标
集新增 `text-indent` / `text-type` / `spacing`;`docs/ui.md` 补 §3.7 的边界说明
与 §3.8 沉浸式阅读。
验证方式。`cd web && npm test`(369 条)、`cd server && npm test`(205 条)全绿;
`npm run build` 通过;`npm run ui:review` 全绿,并新增「收起工具栏: 顶栏与底栏一
起走 / 都不|不可见 / 正文占满整屏 / 右侧仍有快捷按钮」四条尺寸断言(截图见
`docs/ui-review/03-reader-no-chrome.png`),评审夹具改用全角空格缩进,使「段前
空格被去掉」在截图上可见。
Ref: #34
|
2026-09-18 19:09:51 +08:00 |
|
hectorqin
|
cdc95d8d27
|
fix(web,server): 阅读页可读性与面板、翻页、同步频率、事件层级
用户报的八条里,这一提交覆盖了除图标对齐之外的七条。每一条都先做成能复现的
测试或量出来的数字,再改。
## 2. txt 没有处理就渲染,没有默认样式,也无法调整
三层都缺,所以修了三层。
服务端新增 `chapter-html:<n>`,把纯文本渲染成带 p 的片段
(`indexer/formats/text-html.ts`)。空行断段;「上一行以句末标点结尾、下一行以
句子开头」也断段——抓取来的网文常常每行一段且没有空行,少了这条整本书是一块。
章节标题行提升成 h3,否则正文会变成「第二章 落雨雨来了。」。引用仍是
`chapter:<n>`(它同时是阅读位置的标识,改它会把现有读者打回第一章),新增
`format` 字段说明取哪一份;`chapter:` 的 256KB 截断也不再作用于这份渲染结果——
那个截断会把长章节切一半。
客户端新增 `TXT_STYLESHEET`,只在 `format === 'txt'` 时注入 shadow root,作用域
`.txt-body`,所以 EPUB 作者自己的样式一点不受影响。默认两字符首行缩进,用
`2em` 而不是 `2rem`:缩进要跟字号缩放。
阅读设置新增「正文」一节(段落缩进 / 段间距 / 编码),只在 TXT 上出现,前两项
是写在 stage 上的自定义属性,所以调整不需要重新注入章节。
## 3. 翻页异常,往前翻页还会来回跳
`previous()` 用 `scrollTop > 8` 判断「还有没有地方可退」,`next()` 用
`scrollTop < limit - 8` 判断「还有没有地方可进」,然后两边都以 0.9 屏步进。
这三个数字互不相容:forward 落到的位置,back 不认为那是边界。读者看到的就是
来回跳。
改成按**页序号**算术:整屏步进,页边界由滚动**范围**(`scrollHeight -
clientHeight`)而不是内容高度推出。这一个区别是页数和可翻次数对不上的原因——
2.31 屏的一章,按内容高度算 3 页并请求 offset 0/723/1446/2169,而 2169 会被
夹到 1674,也就是第 2 个边界,于是第四次按下的位置计数器不认。
`stepSection` 改成 await:它在跨章时是 fire-and-forget,而 `turnPage` 在
`previous()` 返回后就播放动画——动画演的是读者刚离开的那一章。
## 4. sync 接口会不停的重复渲染,sync 接口调用太过频繁
两处。`onPosition` 每个位置调三次 `patch()`,而 `patch` 会重绘整棵 chrome
树,滚动时每帧三次;合并成一次。
`flushProgress` 无条件 `syncNow()`,于是 1.5 秒一轮 POST+GET,读者只要在读就
一直发。`SyncEngine.schedule()` 去抖并**保证尾次**——没有尾次的话,读者翻完
最后一页就关书,那一页永远发不出去。
## 5/6. 布局样式,以及目录/设置改半屏
面板从 `inset: 0` 改成底部半屏 + 遮罩 + 把手,上半屏正文仍然看得见:选章节、
调字号都是对下面那一页的调整,全屏让读者为了做调整而离开那一页。
状态行从 flex 列里的一行改成浮在正文上的药丸。横条每出现一次就把正文推下去
一行再收回来,也就是把读者正在读的那句挪走。`ui:review` 有一条断言盯着它。
页脚改成两行不同职责:控制行居中、读数是三个固定位置的等宽数字。目录行改成
「序号 + 标题」,当前章高亮一次而不是每行都划线;当前窗口到不了的行标记出来
而不是静静地没反应。
## 7. 点击页面的事件处理层次
一次触摸只属于一个目标,最里面那个赢。两个真缺陷:
- 必须用 `composedPath()` 而不是 `event.target`。书的正文在 shadow root 里,
shadow 边界会**重定向**事件,所以 target 是 shadow host——用 target 写不出
「段落」和「同一个 shadow 树里的按钮」的区别。
- **阅读面自己是个滚动容器**。滚动模式就是让 shadow host 滚起来,于是「可滚动
的祖先拿走手势」这条规则让它吃掉了每一个手势:翻页层级完全正确,但永远
到不了。这是「点击页面没反应」的真正原因。
另外,按到控件的触摸不再被吞掉 click——之前工具栏按钮可见、可点、没反应。
面板打开时它拥有点击:旁边露出的那一条正文不翻页。
## 8. 端到端测试要做 UI 评审
新增 `web/tools/ui-review/`:真实 Chromium(390×844、2x)加载**生产产物**,
对着替身 API 逐屏截图并测量。截图给人看(「只是丑」不是测试失败,但是缺陷),
尺寸给流水线断言(面板不遮住整页、留出 ≥120px 正文、有遮罩和把手、状态提示
不推动正文、控件 ≥28px 且有名字、没有横向溢出)。接进 `client-ui-review`
流水线,只在界面相关文件变化时触发。
这套工具第一次跑就发现了四个真缺陷:`api.toc()` 返回形状不符时 `openStaged`
用 TypeError 打死整个 open(现在回退到 manifest 自己的条目);状态行一旦进入
loading 就再也清不掉(`hideStatus` 的守卫挡住了唯一的出口);翻页步进的 0.9 屏;
以及上面那条阅读面吃掉手势。
## 测试
web 351 项、server 206 项全过。新增:`gestures.test.ts`(事件层级优先级)、
`reader-paging.test.ts`(页序号算术,含子像素取整与滚动范围)、
`mount.test.ts`(单次渲染与卸载后丢弃更新)、`text-html.test.ts`(分段、
转义、标题提升、不截断)。
|
2026-09-18 12:39:45 +08:00 |
|
cnb.dQC5J6ohAGA
|
780849c1b4
|
fix: 修复跨文件系统上传报 EXDEV 错误
|
2026-09-17 17:30:59 +08:00 |
|
cnb.dQC5J6ohAGA
|
b7ffa42c80
|
feat(library): 支持上传书籍与批量管理
|
2026-09-17 16:50:47 +08:00 |
|
cnb.dQC5J6ohAGA
|
282e0426c2
|
feat(library): 新增书库管理页面
|
2026-09-17 12:54:52 +08:00 |
|
cnb.dQC5J6ohAGA
|
0f53088ffe
|
fix(docker): 允许非 root 用户写入挂载的 /data
服务端以非特权用户 `reader` 运行,镜像里构建时做的 `chown /data` 在
`/data` 被 **挂载** 时完全失效:bind mount 的属主来自宿主机目录,宿主机上
`./data` 不存在时 Docker 会以 root:root 新建它,于是服务端写签名密钥时
直接以 `EACCES: permission denied, open '/data/token.secret'` 崩溃退出。
命名卷继承镜像的 chown,所以只有 bind mount 受影响,而 compose 与 README
推荐的正是 bind mount,等于默认路径就是坏的。
改动:
- 新增 `server/docker-entrypoint.sh`:以 root 启动,只在启动时把 /data
这一层的属主改成 reader(不递归整棵树,避免大缓存目录每次重启都被
chown),随后用 su-exec 降权到 reader 再 exec 服务端。服务端仍是 pid 1,
main.ts 的 SIGTERM/SIGINT 优雅退出照常工作。若容器本来就不是 root
启动(`--user`),脚本直接 exec 命令,不尝试改属主。
- `server/Dockerfile`:装 su-exec(约 10KB;alpine 的 busybox 没有等价的
setpriv,util-linux 又不在基础镜像里),加 ENTRYPOINT。去掉 `USER reader`,
因为降权改由入口脚本完成。
- `server/src/config/index.ts`:写密钥失败时,EACCES/EPERM/EROFS 不再只抛
errno,而是报出「DATA_DIR 必须可写」并给出两种修法(改宿主机属主,
或设 READER_TOKEN_SECRET 跳过写文件)。这是自部署最高频的坑,
直接给出下一步能省掉一整轮排查。
- 新增 `server/test/config.test.ts`:覆盖密钥持久化与复用的稳定性、
READER_TOKEN_SECRET 优先时不落盘、目录不可写时错误信息含修法、
DATA_DIR 落在 BOOKS_DIR 内被拒。权限用例在非 root 下运行,
以 root 跑测试时跳过。
- README 增加「故障排查」小节,写明成因与 4 步自查。
验证:`npm run typecheck` 通过;`npm test` 149 项 148 通过 1 跳过
(跳过项即需非 root 的权限用例,已用非 root 单独跑通);以非 root 手动
复现 EACCES 路径,确认错误信息与修法正确;入口脚本的 chown + 降权流程
手动模拟通过。
|
2026-09-17 10:16:11 +08:00 |
|
cnb.dQC5J6ohAGA
|
0370cd98e8
|
feat: HTTP TTS 与双端兼容朗读,书架体验与自定义设置
朗读(Closes #13 的朗读部分)
新增服务端 HTTP TTS 代理与客户端第二、第三个朗读引擎,三者在同一个
SpeechEngine 接口下可切换:
- 服务端 GET /api/v1/tts 代理 TTS_URL 指向的合成服务(edge-tts / Piper
等兼容形态),只代理不内置合成模型;GET /api/v1/tts/voices 回答
「这个实例能用什么」,未配置时返回 http:false 而不是 404。
按句合成、磁盘缓存、上游返回非 audio/* 时明确报错(一个指到网页的
TTS_URL 会以 200 返回 HTML,塞进 audio 里既没声音也没事件)。
/tts 是精确的查询串令牌白名单项之一:<audio src> 无法带 Authorization。
- 客户端 HttpTtsEngine 用 <audio> 而不是 Web Audio,事件驱动推进队列。
- 客户端 NativeTtsEngine 走 Android 外壳新增的 speech 接口(shell v3):
WebView 自带的合成器与系统的是两个引擎,手机系统装好的中文语音经常
不在 getVoices() 里,这是「朗读没有中文声音」最常见的原因。
- speechAvailability 决定 auto 用哪个引擎:native → system → http。
- 原 TtsEngine 抽出 loadQueue/onChunk/onState 为公开字段以结构化满足接口。
书架
- 排序:最近更新 / 最近入库 / 书名 / 作者,中文按数字大小排。
排序规则抽到 shelf-order.ts,离线路径与服务端用同一条规则。
- 密度、作者显隐、封面进度条;搜索一键清空 + 回车即搜;下拉刷新。
- 服务端 BookDto 增加 addedAt,否则客户端离线排序无法复现服务端结果。
自定义设置
- 朗读引擎可选(引擎多于一个时才显示该行);音调行只对有声调的引擎出现。
- 书架设置独立成 sheet,与阅读面板分开。
测试与文档
- server: tts.test.ts(10 例)。web: tts-http / speech / shelf-order
新增 39 例,合计 245 passing。
- android/scripts/check-shell.sh 修正为同时校验嵌套的 speech 接口,
之前它只能看到一个顶层接口。
- README / docs/api.md / docs/architecture.md 补齐三个引擎的取舍、
环境变量与错误码。
Ref: #13
|
2026-09-16 23:29:41 +08:00 |
|
CodeBuddy
|
b9b9c63605
|
merge: 把 main 合并进按窗口读取分支,并修正目录书页契约的两处缺陷
冲突只有两处文档(README 的测试计数与端点清单、docs/api.md 的 /manifest 说明),
两边各自都是对的,所以按并集处理:计数取本分支的实际值,端点清单保留 main 新增的
/items、/toc、/file、/library/formats。
合并后按契约把两边一起验了一遍,发现目录书这条链路上有两个会被静默吞掉的缺陷,
都是「服务端仍回 200、读者看到的是坏内容」那一类:
1. 「卷」的判据里有一句 `images === 0`。整卷系列里放一张封面扫描是常态,于是
`第01卷.cbz` 与 `第02卷/` 并列、旁边再有张图时,整个系列会被判成「不是漫画」,
而它也不是别的格式的书——书架上直接消失。判据改成按来源问:压缩包的页就在包里,
一个压缩包本身就是一卷;卷子目录要往下看才知道有没有页。散图仍由 MIN_IMAGES 管,
既不能否定同级存在的整卷,也不该在自己是唯一页源时被当成书。
2. 页的资源引用被 API 层按「列表下标」拼成了 `page:<index>`,而这个格式的引用形态是
`page:<卷>:<页>`。「一个压缩包算一页」意味着一个卷在列表里的下标与它在卷里的位置
无关,于是按 `page:1` 去问一卷时格式处理器解不出引用,异常回退到「当普通文件发」,
客户端请求一页、拿到的是那个压缩包——状态码 200,内容不对。引用现在由同一次页面
遍历给出(manifest 的 files[].ref),/file 原样回传,不再有第二处实现。
顺带修正三处文档与实际不符的地方:`.cbr`/`.rar` 并未被解析(原「不做」清单暗示只有它
不做)、嵌套深度措辞与实现(读到第 4 层,且内嵌压缩包不展开)、README 的测试计数。
server 137/137(typecheck + test + build),web 179 vitest + 29 node:test(typecheck + test + build)。
|
2026-09-16 22:49:45 +08:00 |
|
cnb.dQC5J6ohAGA
|
c18664f1e3
|
feat(reader): 按窗口读取、真机可编译的 Android 壳,以及分章契约的收口
两件事:把 PR #8 里那四项合并进来(A 选项),然后把 issue 里一直没落地的
「一次只取一章」从服务端契约一路做到客户端。
## 一、PR #8 的四项(Android 外壳最值钱)
- Android 原生整页图渲染:NativePageView + 桥接 v2(renderPage/hidePage)。
只画整页图,EPUB 永远留在 WebView;画不了就返回 false 回退,不出现空白页。
- 离线队列:脏集合语义固定为契约,补上「请求在途时位置被改写」的三条用例。
- TXT 编码探测:GB18030 与 Big5 都解一遍再按损坏证据评分选优。
原来「先 GB18030 失败再 Big5」永远走不到第二步——两者字节范围重叠,
同一串字节通常两边都能解出合法字符,繁体书会整本乱码。
- 格式嗅探:magic number 优先于扩展名,并明确拒绝非书籍的媒体容器。
## 二、Android 壳第一次真的编译通过
上一轮说「没有 JDK/SDK,只写了契约」,这一轮补上了环境并真的编译。
编译立刻暴露三处——都是只有编译才能发现的:
- colors.xml 的注释里写了 --reader-paper。XML 禁止注释中出现双连字符,
AAPT 直接拒绝整个文件。Android 壳此前从未构建过,所以从未发现。
- NativePageView.kt 用了 Uint8Array。那是 JavaScript 类型,Kotlin 里不存在;
桥接的另一侧是 ByteArray。
- AndroidManifest.xml 引用了 @mipmap/ic_launcher,而资源里没有这个文件。
三条都让 :app:assembleDebug 失败,也就是说这个 APK 此前根本产不出来。
现在是 BUILD SUCCESSFUL,产物 3.6MB。
## 三、分窗读取:从契约做到客户端
服务端:
- /manifest 省略 group 现在等价于 group=0。原来省略意味着「发全部条目」,
于是一本 1200 章的书在它最需要快的那个请求(打开书)里收到 1200 条记录,
等于分窗对不传参的客户端完全失效。要完整结构用 ?group=all。
- /toc 在没有 OPF 标题映射时回落到章节自身的第一个标题。手作 EPUB 大量
没有标题映射,原来整本目录会读成「第 1 章 … 第 300 章」,等于没有目录。
- 扫描时的 spine 读取不再解析 NCX 与 nav:扫一遍全库本该只读一次包文档。
- 尾部 Range(bytes=-N)在超过 8MB 时退化为整包响应并撤回 Accept-Ranges。
前向流无法跳到文件末尾,服务这种请求意味着为三个字节读完整个 400MB 漫画包。
客户端:
- 新增 formats/remote-zip.ts:走 HTTP Range 的只读 ZIP,让浏览器端也能不下整本书。
- 新增 formats/windowed.ts:分窗文档。分窗是传输边界,目录必须完整,两者分开
—— 合在一起就是原来目录面板显示「第 1 章 – 第 40 章」的原因。
- reader-screen 接上 /toc,并且点击目录项会先取对应窗口再跳。
## 验证
server: 133/133(typecheck + test + build)
web: 179 vitest + 29 node:test(typecheck + test + build)
android: BUILD SUCCESSFUL,app-debug.apk 3.6MB
shell: 契约双向校验 + 渲染模式词表校验通过
|
2026-09-16 19:51:38 +08:00 |
|
cnb.dQC5J6ohAGA
|
5f1e29a3e5
|
fix(server,docs): 目录书页面清单成为契约,修正三处会让书静默消失的缺陷
|
2026-09-16 18:40:37 +08:00 |
|
cnb.dQC5J6ohAGA
|
ca68a03e5d
|
merge: 解决 auto/init-server-scaffold-81b3 与 main 的冲突
两个分支各自演进:一侧引入格式注册表、H5 客户端与 Android 外壳,另一侧
引入漫画目录书与逐页取图。冲突集中在扫描器、metadata、library 路由和 web
客户端,逐项按「两边能力都要保留」的原则合并。
合并后修复了 4 个被冲突掩盖的真实缺陷:
- scanner 同时保留了两套目录书实现,类型检查与测试均无法通过;改为统一
走格式注册表,目录书在文件遍历之前发现,其内容不再重复入库。
- /manifest 的 files 对漫画目录书返回的是文件夹路径,客户端无法逐页取图;
改为由 manifest 派生真实页路径。
- 单个文件端点只认 book_files 行,漫画目录书的每一页都 404;改为目录书
额外认领其目录下的路径,并修正路径归一化使穿越尝试仍被拒绝。
- 目录书判定只看图片数量,会把夹着一本书的目录整体吞掉;改为注册表驱动
的判定,含书籍文件的目录始终是书架。
同时修复了 web 端两套测试运行器互相无法执行对方用例的问题(vitest 与
node:test 混跑会静默跳过),以及 web 侧 5 处类型错误。
Ref: #9
|
2026-09-16 18:04:11 +08:00 |
|
CodeBuddy
|
5e8d14b460
|
fix(web): 用 iframe 承载章节文档,浏览器实测逼出来的四处修正
真机(Chromium,390x844)跑通整条链路后,之前"看起来对"的地方全都露馅了。
这一轮把渲染层按实测重做,四个都是必须修的:
1. **章节必须是真文档,不能是 shadow DOM 里的片段。**
章节里的 <img src> 和 <link rel=stylesheet> 是浏览器自己去请求的,
没法带 Authorization 头。把它们注入宿主文档的结果是每个插图、每个
样式表都 401 —— 书渲染成无样式无图。iframe 是真浏览上下文,它的
子资源请求是普通导航,URL 里带什么就发什么。所以:外壳用 shadow DOM
(挡的是客户端样式往书里漏,这是它真正能挡的方向),书本身用 iframe。
2. **子资源的 token 只能走 query。**
服务端新增 `queryTokenAllowed`,白名单式放行:仅 GET、仅
/api/v1/books/*/assets|cover|content。触发扫描、写元数据这类端点一律
不接受 URL 里的 token —— 那才是真正会泄到日志和 Referer 的地方。
新增 5 个测试固定这条边界。
3. **`column-fill: auto` 需要确定高度,否则根本没有分页。**
body 不设高度时它会撑到内容高度,整章排成一列,scrollWidth 等于
clientWidth —— 翻页手势完全无效,一章只显示一页。修完 390px 宽的一章
是 3732px / 9 页,翻页步长精确 422px(390 + 32 gutter)。
同时去掉 scroll-snap:吸附目标是列边缘而不是我做出来的页边界,
会导致 scrollLeft 落在两页之间、甚至一次跳两页。
4. **`pageWidth` 是上限不是实际宽度。**
手机上 720px 的列比屏幕还宽,同样退化成"一章一页"。取
min(读者设置, 实际宽度)。
另外发现并按格式分流修好的:PDF(kind=document)之前落进了图片翻页器,
读者看到的是一个破图;现在走 PdfReader。
封面 <img> 也漏了 token,一并改成 api.coverUrl()。
顺带把一个设计缺陷拆开了:目录不再从 manifest 的 groups 取。
groups 是传输分窗,它把目录显示成「第 1 章 – 第 40 章」——服务端的分页
边界漏进了 UI。新增 /books/:id/toc,按格式返回真正的目录
(epub 的 spine 标题、txt 的章节、漫画的卷),与分窗互不干扰。
|
2026-09-16 15:28:34 +08:00 |
|
CodeBuddy
|
0a17174fb8
|
fix(server): 让解析器修复能真正到达已入库的书
改 EPUB 读取时发现两个连在一起的坑,都会静默留疤:
1. `spineLength()` 用内存里的 zip 打开,但里面调用的 `readPackage` 又要求
从文件读,于是每本 epub 的 page_count 都是 null 且不报错。
`null` 同时也是"这个格式本来就报不出页数"的合法答案,下游无法区分。
2. 更麻烦的是:即使修好 1,已入库的书也修不回来。变更检测基于内容哈希,
而解析器修复不改变任何字节,扫描恰好会跳过最需要重读的那些文件。
于是坏结果永久留存——这正是这个 bug 在真实书库里的表现。
修法是给解析结果带上版本号:`book_files.parse_version` 记录是哪一版解析器
写下的,`PARSE_VERSION` 一旦提升就强制重解析一次。自部署用户不会按时升级,
所以版本得跟着数据行走,不能只活在进程里。
配套:schema 加一段可重复执行的增量迁移(SQLite 不支持 ADD COLUMN IF NOT
EXISTS,靠 duplicate column 错误判断已执行过),旧库能原地升级。
新增的日志出口 `lib/log.ts` 是因为扫描器和解析器跑在定时器里,没有请求
可以借 logger;静默正是 1 得以存在的原因。
|
2026-09-16 14:48:35 +08:00 |
|
CodeBuddy
|
d0a475919d
|
feat(web): H5 客户端,精排 epub 渲染 + 多格式阅读
用户的三个问题在这里落地:
1. 「H5 是一个完整的客户端」——同一份构建产物既由服务端托管(`/` 直接
就是完整阅读器),也可被 Android 外壳打进 assets 复用,两种宿主只差
一个 serverUrl。
2. 「纯 H5 时 epub 等资源是否需要 iframe 或 shadow DOM」——用 shadow DOM,
但不是为了隔离出版方样式(那做不到也不该做:书必须能给自己排版)。
它挡的是反方向:客户端自身的 CSS 不会漏进书里。iframe 只在 PDF 那一处
用,因为 PDF 需要自己的浏览上下文,理由写在 render/container.ts 里。
3. 「Android 端 epub 用 H5 渲染,其他类型用原生」——按 kind 分流,
capabilities() 明确声明:reflowable 走 WebView,paged/document 在
Android 交回原生。理由写进 reader-view.ts:WebView 每页一个合成层,
300MB 的画册在一台中端机上就是 OOM。
服务端同步配合的两处:
- 资源流式下发 + HTTP Range
- epub 按包内路径寻址、items/manifest 分窗
前端无框架:渲染热路径是命令式 DOM 操作,引框架只会把"最小覆盖 CSS"
的策略搞复杂;最终产物 35KB(gzip 10.7KB)。
|
2026-09-16 14:45:04 +08:00 |
|
CodeBuddy
|
80454c2491
|
perf(server): 按需读取书籍容器,章节/页流式下发
EPUB 与漫画此前每次 manifest / assets 请求都整包解压(EPUB 走 JSZip)。
一本 50MB 画册的每次翻章都会把整个压缩包读进内存;扫全库时更糟。
- EPUB 改走项目自研的 zip-reader,只读中央目录 + 按需解压条目
- 新增 ZipArchive.openStream():stored 直接读,deflate 走流式 inflate,
单页取图不再需要整包驻留
- 资源响应支持流式下发,seekable 资源支持 HTTP Range(断点续传、
PDF 跳页、漫画跳卷都靠它)
- epub 章节改为按包内路径寻址(xhtml:OEBPS/ch1.xhtml),不再用 spine 下标:
分窗加载后下标在不同窗口里指向不同章节
- items/manifest 支持 ?group=N 分窗,group 自带 offset,客户端拿到单窗
也能定位到全书位置
- /manifest 直接带回可寻址结构,打开一本书从两次往返降到一次
|
2026-09-16 14:40:28 +08:00 |
|
cnb.dQC5J6ohAGA
|
664141f0f1
|
feat: 支持 txt 与漫画等多种格式,格式层改为注册表
原先只认 .epub 和 .pdf,本地书库里占很大比重的 txt 长篇与漫画
(cbz、图片目录)会整片「消失」—— 用户能看到文件在 NAS 上,却在
书架里找不到,这是最让人困惑的一类问题。
格式列表硬编码在三处(扫描器白名单、parseBookFile 分支、content-type
映射表),加一种格式要同时改三处,漏掉任何一处就得到一个「扫到了但读不
出来」的安静 bug。这次把格式收进注册表,并补齐四种格式。
新增格式
- cbz / zip:按包内页序自然排序翻页,扫描时不解压到磁盘
- 漫画目录:一个目录一本书,子目录为卷,支持卷/页两级寻址
- txt:UTF-8 严格解码失败才回退 GB18030(UTF-16 BOM 也识别),
按中文网文章节标题分章,正文可按章节或字节偏移读取
- 单张图片按单页读物入库
- 明确不支持 cbr/rar:需要非自由解压实现,与单容器约束冲突
格式层
- registry.ts 定义 handler 契约(parse/manifest/asset)与扩展名路由,
扫描器与 HTTP 层只与它对话;扩展名白名单、能力声明、遍历过滤都自动派生
- zip-reader.ts 自研只读 ZIP,只用 node:zlib。JsZip 会把整包解压进内存,
而漫画动辄几百 MB,首次扫描真实漫画库会 OOM
- natural-sort.ts 自研自然排序:page10 必须排在 page2 之后,
否则整本漫画的阅读顺序是乱的;不用 localeCompare 是因为 ICU 行为
在不同平台上不一致
- epub 章节返回前重写章节内的相对资源引用,否则 WebView 里图片/字体/
样式全部 404;绝对 URL、data: URI 与文内锚点保持原样,脚注不会坏
修复
- 目录型读物认领后,其内部文件从索引中排除,否则同一部漫画会以
「系列 + 每卷 + 每页」的形式重复出现
- 目录认领顺序改为从外往里:反过来的话一部漫画会变成 N 本独立卷
- 主键新增原地编辑规则:txt/漫画没有内嵌标识,追加一章会算出新 id
并把读者进度留在旧记录上。现在同路径原地编辑算更新,移动/重命名
仍走内容哈希规则,所以进度都保得住
- 损坏的 .cbz 仍然入库并记录失败原因,而不是静默从书架消失
- PDF 不再用正则猜页数(会匹配到无关的 /Count 并报出错误页数,
让客户端分页错乱),改为诚实上报 null
- 清理已迁出的 parseBookFile / pdfInfo / SUPPORTED_EXTENSIONS
测试
- 新增 65 个用例(共 92 个):真实 zip 与真实文件,不用 mock;
fixtures 由测试自己生成,不需要签入二进制
- 覆盖自然排序、编码探测、ZIP 边界(ZIP64/加密/超大条目)、
目录认领、原地编辑、只读约束、各格式的读取端点
|
2026-09-16 14:31:15 +08:00 |
|
cnb.dQC5J6ohAGA
|
2233b295bb
|
feat: 初始化自部署书库服务端
按产品设计 v0.2 落地服务端,覆盖第一版范围中的扫描、元数据、权限与同步。
核心决策:
- 书籍主键用 dc:identifier + 内容哈希,不用文件路径。
重命名/移动不丢进度,副本自动合并,重新编码的文件保持独立。
- 库目录只读挂载,写操作全部落到 DATA_DIR。
启动时断言 DATA_DIR 不在 BOOKS_DIR 内,路径解析统一走 resolveInside
拦截逃逸,扫描不跟随符号链接。
- 元数据优先级:手动修改 > 内嵌 > provider > 文件名解析。
手动修改独立成表,扫描永远碰不到,可单字段撤销。
- 中文文件名解析采取保守策略,无证据时不猜测作者方向。
- 离线合并用 LWW,笔记删除写墓碑,批次单事务。
- 无原生编译依赖:node:sqlite 与 scrypt 均为内置模块。
交付:
- Fastify + TypeScript 服务端,Docker 单容器
- 27 个自动化测试用例全部通过
- AGPL-3.0,README 做到一条命令跑起来
- CNB 流水线:typecheck / test / build
|
2026-09-16 14:07:25 +08:00 |
|