hectorqin and Claude Opus 5.5
120827bad6
docs: bring every document in line with the code
...
Audit of README, docs/, android/README.md, android/contract.md and CLAUDE.md
against the current server, web and Android code: V3 navigation and settings
locations, Docker Hub release images, GitHub clone URL and CI, missing API
endpoints and error codes, architecture of lazy chunks/themes/sources, media
pages and routes, the Android bridge contract, and the new file-management and
member-upload permissions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-30 02:03:45 +00:00
hectorqin
875fec84dd
fix(media): sync prototype routes and review tooling
2026-10-08 09:51:42 +08:00
hectorqin
dec8ec8c44
docs: document media features and remove remote source plugin guides
2026-09-28 10:32:52 +08:00
hectorqin
1d86efab34
feat(reader): add selection annotations and server-side book search
2026-09-24 16:03:35 +08:00
hectorqin
0b7f901db5
chore(server): change default listening port to 5888
2026-09-24 14:50:13 +08:00
hectorqin
72c4405435
chore: keep host repository plugin agnostic
2026-09-22 17:47:21 +08:00
hectorqin
5772014d3f
docs: split bilingual readme and getting started guides
2026-09-22 17:12:49 +08:00
hectorqin
4af639d3b8
docs: document streamed source search and upload semantics
2026-09-22 16:54:18 +08:00
hectorqin
be219e5090
docs: align library write model and deployment guidance
2026-09-22 16:49:06 +08:00
hectorqin
5ecf918551
feat(plugins): support independently installed npm providers
2026-09-21 10:29:17 +08:00
hectorqin
59faec336a
merge: 同步远程阅读界面更新并保留书源配置样式
2026-09-21 09:39:39 +08:00
hectorqin
c192fc2f12
feat(sources): add configuration pages and chapter updates
2026-09-20 23:59:12 +08:00
hectorqin
88f7a437f8
Merge branch 'main' into codex/source-plugins
...
解决 web/src/ui/reader-chrome.tsx 与 web/test/reader-chrome.test.ts 的冲突:
- 导入合并为 toolkit 的 IconButton/IconTextButton/SectionTitle 与 icon.tsx 的 Icon
- 目录面板保留章节出版物的「刷新目录」按钮
- 设置页保留 main 的「界面/阅读设置」分页与「浅绿」主题
- 测试中两边的用例均保留
2026-09-20 22:51:41 +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
d60d7e67f9
feat(reader): 配置四角阅读信息
2026-09-20 17:36:59 +08:00
hectorqin
da1aa15174
fix(reader): 稳定阅读位置并重整手机阅读界面
...
固定工具栏显隐时的正文区域,保存最新阅读偏移并抑制亚像素量测抖动。重组底部四入口、半屏面板和章内滑杆,采用 Lucide SVG 图标并新增浅绿主题;补齐 PDF 内容传递。
验证:构建通过,498 项单元测试、29 项接口及分页测试、30 项 UI 检查通过。
2026-09-20 17:02:40 +08:00
hectorqin
558f44e206
feat(web,server): 书架默认按最近阅读排,书库拆成预览与文件两页
2026-09-19 14:17:55 +08:00
hectorqin
3c3e461a49
feat(web): 界面图标换成自带字体,补 UI/UX 规范并统一设计令牌
...
按钮里放的是 Unicode 字符——`☰` `⚙` `✕` `⟨` `⟩` `+` `🗂` `📁 ` `⮝`——
和 emoji。八个字形来自八个字族,光学重量各不相同;`🗂` `📁 ` `⏮` 在部分平台
被渲染成彩色的,而一个彩色 emoji 出现在单色工具栏里,是「没有设计过」最直接的
信号。不同平台的字体回退还不同,同一个界面在 Android 和桌面浏览器上长得不一样。
此外样式表里所有按钮都没有 `:focus-visible` 规则,键盘走一遍看不到焦点在哪。
所以这次做三件事:图标改成自带字体、把缺失的交互状态补齐、把散落的字面量收成
设计令牌,并把规范写成文档。
图标用字体而不是内联 SVG,理由是**一致性可以被强制**:字体把字形变成字符,它
继承 `color` 和 `font-size`,没有自己的几何,因此没有可以漂移的地方;而三十个
内联 SVG 组件里,颜色、尺寸、线宽每一项都是单个字形可以漂移的位置。字体是
bundle 里的一个 16KB 相对路径文件,`file://` 下能加载,与「一份产物两个宿主」
的既有约束一致。
字形由 `web/tools/icons/` 下的脚本从描边路径编译:路径 → 折线 → 圆头矩形 →
精确布尔并(Clipper)→ TTF。先做过一版「栅格化 + 边界追踪」,失败了:笔画宽度
只有一个栅格单元,而逐单元的边界规则无法表达「边界在一格宽的图形上左右交替」,
结果是一个圆被追踪成 58 段碎片。精确布尔没有这种情形。
放弃的方案:内联 SVG(前面说的漂移问题)、把图标做成 React 组件并逐个手绘
(每个都是独立的几何,光学重量必然不一致)、从 CDN 引用图标字体(本产品必须
在无外网环境下工作)。
界面改动:
- `Icon` 组件 + 37 个字形,码位表由生成器写出,测试直接解析字体 `cmap` 核对
- `IconButton` 的 `icon` 是字形名而非任意 `children`,传错名字是编译错误;
`label` 必填,字形始终 `aria-hidden`
- 补上全部按钮的 `:focus-visible`(本次之前完全缺失)
- 间距/字号/动效收成令牌;`prefers-reduced-motion` 通过折叠时长令牌实现,
而不是给每条动画各加一个媒体查询
- 悬停包进 `@media (hover: hover) and (pointer: fine)`,避免触屏上按钮停在悬停态
验证:`web` 336 个测试通过(新增 9 个图标契约测试);`typecheck`、`build`、
`check-shell.sh`、`verify-web-assets.sh` 全过;字体已确认被打进 Android assets。
真浏览器(Chromium + 真实服务端)实测了书架、书库管理、阅读器三个屏幕的浅色与
深色主题,以及键盘 Tab 下的焦点环。产物 259.5KB → 259.5KB(gzip 81.4KB),
新增 16KB 字体、CSS 减到 24.4KB。
Ref: #29
2026-09-18 07:08:02 +08:00
hectorqin
ecb2ef4806
fix(web): 修复阅读页章节跳转失灵,补齐上一章/下一章
2026-09-17 23:52:11 +08:00
cnb.dQC5J6ohAGA
dbaac99b45
feat(web): 界面加路由,首页重做
...
首页(书架)此前是纯命令式拼出来的,而整个客户端没有任何路由:URL 从头到尾不变,
返回键、刷新、分享、收藏一概不成立。
## 路由
`ui/router.ts` 把 fragment 解析成 route,shell 渲染 router 报上来的那个:
#/shelf 书架
#/book/<id> 阅读器
#/library/<路径> 书库里的目录
用 fragment 而不是路径,是因为同一份产物要被两个宿主加载,而两个都服务不了任意路径:
Android 从 appassets.androidplatform.net 加载(那里没有服务端),H5 由 reader 服务端
托管(没有客户端路由回落)。路径式路由会在 vite dev 里正常、在两个生产宿主里 404。
返回走应用自己的屏幕轨迹,不走 history.back():应用里的「返回」是「上一个屏幕」,
而浏览器历史里可能有登录表单或别的站点。轨迹为空时(新标签页里粘贴的链接)替换成父级,
而不是压入——否则那个链接会永远压在书架后面。
走进书库子目录用 replace:书库是一个带面包屑的屏幕,走目录是同一个屏幕改自己的参数。
压栈的话,读者走过四个文件夹再按返回会逐层退出来。
带着链接进来而尚未登录时,route 存在 App.pending 里等在登录页后面,登录后直接落到
目标屏幕,而不是落到一个他从未见过的书架。
Android 外壳的返回手势不需要改动:它看的是 WebView 的 canGoBack(),而路由现在真的
往 WebView 历史里写——之前那段注释描述的行为,从这次起才成立。
## 首页
- 先看到书,再看到控件:标题和封面在最上面,搜索在后,排序与数量贴在它们作用的那一节上。
旧版把搜索框、数量、四个排序 chip 依次排在封面之前,三行控件才轮到第一本书。
- 排序栏是唯一吸顶的东西,它承载的是读者会滚回去改的两件事。
- 空状态给一条出路(打开书库管理 / 刷新 / 清除搜索),而不是一句「书库还是空的」。
- 冷启动画骨架屏,且按当前密度画——密度不对的骨架屏会让读者看到布局动两次。
- 封面缺图时把书名画成封面,而不是一个灰色方块。
## 顺手修掉的 5 个真缺陷
1. mountUI 在 unmount 之后仍会渲染。屏幕是异步加载的,离开后那条迟到的响应会把
整棵树画进一个刚被清空的容器;书架迟到的响应会把自己一屏封面画进书库管理器。
2. --safe-left/-right/-top/-bottom 从未在生效的样式表里定义过,只有一份没人 import 的
死样式表定义过。未定义的自定义属性会让整个 calc() 失效并静默丢弃声明。
3. 封面在 flex 列里没有显式宽度,aspect-ratio 盒子因此收缩到内容宽度:只有书名的封面
塌成 42px、有图的占满卡片,整排看起来参差不齐。
4. 阅读列在宽屏上被拉伸到 9 列 45px 的封面。现在内容列封顶 68rem,封面随断点变大。
5. 删掉 web/src/ui/styles.css——414 行从未被引用的旧样式,以及 .book-highlight 等
已经没有任何代码产出的规则。
## 验证
- web 全量 284 项测试通过(新增 router 19 项 + 链接契约 4 项)。
- typecheck / build / check-shell.sh / build-web-assets.sh 均通过。
- 真浏览器实测:深链直开、刷新不丢、返回离开屏幕、登录后落回目标、书库走目录、
空搜索、粘性工具栏、深浅色、390px 与 1440px 两档宽度。
- 体积:251KB → 252KB(gzip 79.1KB)。
2026-09-17 18:49:53 +08:00
CodeBuddy
586bec25b1
refactor(web): 界面层迁移到 Preact,排版层保持命令式
...
书架、登录、书库管理、以及阅读器的铬层(顶栏/页脚/目录/设置面板/朗读条)
从「构造函数建节点 + 实例字段 + 若干 render*()」改成组件树 + 单一状态。
阅读舞台——分页、注入文档、shadow root、量测列宽——仍然零框架。
为什么换:
- 同一份状态有多个副本。分段控件的选中态既在 settings 又在 aria-pressed 上,
靠点击处理器手工同步;LoginScreen 的通知/错误/注册开关/按钮文案是四个节点
被五个地方改,漏一处不报错。
- 重建与状态互相矛盾。朗读引擎行要在 HTTP 能力探测回来时重新生成,而整块
面板重建会丢掉面板滚动位置,于是长出一套「只替换 [data-speech=group]」
的定点重建;改成组件后探测只是改状态,diff 天然保住滚动位置。
边界(写在 ReaderScreen 里,不靠自觉):
- stage 由类持有并交给 ReaderView,只通过 ref 回调挂进组件树;组件决定它
放在哪,内容一概不碰。mountUI() 是组件树与类式屏幕之间唯一的接缝,
路由仍不认识框架,测试仍打在真实 DOM 上。
配套:
- toolkit.tsx:IconButton / Button / Segmented / SwitchRow / Scrim 这些原语
各把 a11y 与状态细节收在一处,消掉两处手工 aria-pressed 同步。
- vendor/preact.ts:框架唯一入口,只 re-export preact 与 preact/hooks,
无 preact/compat、无 react-dom、无额外 signals 依赖。
- tsconfig: jsx=react-jsx, jsxImportSource=preact。
- 修一处回归:书库管理的状态行改由 listing 推导,写入结果跨重载保留。
验证:web 全量 260 项测试通过(manager.test.ts 断言逻辑未改),
typecheck、build、android check-shell.sh、build-web-assets.sh 均通过。
产物 235KB → 245KB(gzip 71.2KB → 77.2KB)。
2026-09-17 17:52:18 +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
cnb.dQC5J6ohAGA
75718e9fb7
merge: 合并 main 并解决阅读界面与分窗读取的冲突
2026-09-16 23:11:10 +08:00
cnb.dQC5J6ohAGA
058bee57a5
feat(web): 丰富阅读界面设置并支持系统 TTS 朗读
2026-09-16 22:55:58 +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
02453c93d3
docs: 更新 README / 架构 / API 文档
...
记录本轮的四项决定与三个坑:
- 排版策略:出版方优先、变量层可被书拒绝、无 !important
- 渲染分工按内存而非速度划分
- 外壳用 Shadow DOM、书用 iframe 的完整理由
- column-fill 需要确定高度的实测结论
- 分窗与目录分离的原因
- 解析器版本机制与它解决的静默陈旧问题
- 子资源 query token 的白名单边界
2026-09-16 15:49:22 +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