dsh-reading
在侧边栏里读 EPUB、MOBI、AZW3、TXT、PDF 五种格式的书:分页、目录跳转与书内链接跳转、划线笔记与书签、全文搜索、带原文锚点的 AI 问答、按章蒸馏,并能导入 Legado 格式书源按章抓取在线书籍,随包还带一个陪读教练 skill。PDF 只支持文字版,扫描件与加密 PDF 会被拒绝。
安装
dsh plugin --profile web add dsh-reading
沙箱测试结果
| 阶段 | 结果 |
|---|---|
| 1 安装 | ✓ |
| 2 列装 | ✓ |
| 3 挂载 | ✓ |
| 4 启动 | — |
四阶段在隔离沙箱里跑:安装(有网)→ 列装 → 挂载 → 启动(后三步断网)。
README
dsh-Reading
在 DeepSeek Harness(下称 DSH)里读书的插件。
导入 EPUB / MOBI / AZW3 / TXT / PDF,分页读,做目录跳转,记住读到哪,划线、写笔记、加书签。
没有账号,没有服务器,没有遥测:书和笔记都在你自己的磁盘上。
状态:0.9.6,M1「能读书」+ M2「能划线」+ 搜索/标注交互 + 手机与多端 + AI 状态诊断与读书蒸馏 + 书源搜书,
支持五种输入格式(EPUB / MOBI / AZW3 / TXT / PDF 文字版)。
这一版加上:Legado 书源的导入与搜书(用自己的源,抓目录、按章取正文)、
阅读设置按「文字 / 纸张 / 翻页」分组。插件不内置任何书源,也不带默认站点。
跨端同步(WebDAV)还没做,见 [路线图](#路线图)。
它解决什么问题
DSH 里能读文档,但读书是另一件事:一本 30 万字的小说需要分页、需要目录、
需要「上次读到哪」、需要把喜欢的句子划下来日后翻回去。
dsh-Reading 就是补这一块。它只有一条设计原则,写在所有实现细节之前:
锚点是 EPUB CFI,不是百分比,不是「第几个字符」。
百分比会漂:改字号、换设备、换窗口宽度,百分比指的位置就变了。
CFI 是 EPUB 规范里定义的结构化位置(epubcfi(/6/4[ch01]!/4/2/2/1:12)),
它指「第 4 个 spine 项、body 下第 2 个 div 的第 2 个 p 的第 1 个文本块的第 12 个字符」。
改字号不影响它,换设备也不影响它 —— 前提是实现严格按规范(下面「它凭什么可信」讲这件事)。
安装
前置
项 · 要求
DSH · >= 0.2.0-rc.1 < 0.3.0(CLI dsh --version 自查)
Node.js · >= 22.19.0
平台 · 桌面版 DSH(Web UI;面板挂在左侧栏图标条)
从本地目录装
桌面版(Electron,profile 名通常是 desktop):profile 由应用独占管理,CLI 的
dsh plugin 会拒绝操作它(profile "desktop" is managed exclusively by the Electron application)。
请在应用里的插件管理界面安装这个目录。
命令行版 / 自建 profile:
dsh plugin --profile web add /path/to/dsh-Reading
(--profile 放在 add 前面;写成 dsh plugin add <dir> --profile web 会被当成 pnpm 参数、报缺 --profile。)
装完重启 DSH:插件清单在启动时确定,运行期改文件不会热载。
重启后左侧栏底部图标条里会出现一本打开的小册子,点它进书架。
卸载
dsh plugin --profile web remove dsh-reading
桌面版同样在插件管理界面里卸载。
卸载不会删你的书和笔记。它们在自己的目录里,见下节。
数据在哪
默认 ~/.dsh-reading/(可用配置项 libraryDir 改):
<libraryDir>/
├── settings.json # 阅读偏好:字号、行高、主题、栏宽
└── books/
└── <bookHash>/ # sha256(epub 字节) 的前 16 位
├── book.epub # 原始文件
├── meta.json # 书名、作者、spine 表
├── progress.json # 读到哪(CFI + 百分比)
├── annotations.jsonl # 划线 / 笔记 / 书签,一行一条
├── qa.jsonl # 给将来的 AI 问答留的位置(当前只读写结构)
├── quarantine.jsonl # 解不开的锚点,只写不读
└── history/ # 被覆盖前的旧文件,保留最近 20 份
三条约定:
1. 备份就是复制这个目录。 没有数据库,没有二进制格式,全部是 JSON / JSONL。
2. quarantine.jsonl 只增不减。 解不开的锚点不进渲染,但记录永远留着 ——
那是你的数据,坏了也不该被悄悄扔掉。
3. 删除是墓碑,不是删行。 annotations.jsonl 里被删的记录变成 "deleted": true,
文件里那一行还在。这是将来做多设备合并的前提:没有墓碑,另一台设备会把它「复活」。
配置
装完就能用。要改的话,在 profile 的 cordis.patch.yml 里按行 id 覆盖:
- id: reading
name: 'dsh-reading'
config:
libraryDir: /Volumes/books/dsh-reading # 默认 '' = ~/.dsh-reading
maxImportBytes: 209715200 # 默认 200 MB
改完同样要重启 DSH。
它能做什么
书架 — 列表带封面、进度圈、最近阅读。点封面进阅读视图。
「导入」选本地文件(也可以直接拖进窗口)。
对着卡片右键(或点右上角 ✎)是编辑入口:改书名、作者、简介,填分类、系列、
打标签、标成「我的珍藏」,换封面图,或者把封面恢复成默认的纸封面。
封面一律按 2:3 摆,同一行不会因为谁名字长就把旁边那张挤歪。
左上角两级下拉可以筛书架:全部 / 我的珍藏 / 按系列 / 按作者 / 按分类 / 按标签。
筛选是纯前端的,不动书的数据。细节见 [docs/13](docs/13-书籍信息与筛选.md)。
「导入」面板分三段:从电脑选文件、粘贴一条下载直链、或者用你自己填的书源搜关键词。
插件不内置任何站点,也不带默认书源——你填什么它抓什么,见 [docs/12](docs/12-找书与网络导入.md)。
格式 — 能读五种:
格式 · 呈现 · 说明
EPUB · 原样渲染 · 插图、版式都保留
MOBI / AZW3 · 重排为纯文字 · 没有插图与原版式,书架上标「重排」
TXT · 重排为纯文字 · 自动认 UTF-8 / UTF-16 / GB18030,按「第 X 章」切章
PDF(文字版) · 重排为纯文字 · 扫描件与加密 PDF 读不了;表格、分栏会退化成流水文字
导入按钮写「导入图书」,因为这五种格式都能导——不写「导入 EPUB」,免得让人以为只认 EPUB。
重排书的划线、笔记、进度和 EPUB 完全一样——锚点仍然是 CFI。
细节与取舍见 [docs/08](docs/08-格式支持-TXT与MOBI.md) 与 [docs/11](docs/11-格式支持-PDF.md)。
阅读 — 分栏分页;翻页可点正文两侧的窄边(悬停浮出 ‹ ›)、按键盘左右、滚轮或触控板两指横滑(一次手势只翻一页)。
字号、行高、栏宽、明暗都在阅读视图的设置里调,改动即时生效、立刻落盘。
目录 — 章节树,当前章节高亮,点一下跳过去。目录分三种来源,面板上会标明是哪一种:
- 真目录(EPUB 自带的 nav / NCX):带层级;
- 自动识别:TXT / MOBI 这类没有真目录的书,按正文里的「第 X 章」标题认出来,标「自动识别」;
- 没有目录:正文里连章节标题都没有的书,面板会直接说「这本书没有目录」,不编造一个假的章节列表。
书里的目录页也能点 — 正文里那页手打的「目录」,只要章节名本身是链接(带下划线那种),
直接点那行字就跳到对应章节,跨文件的锚点也认;外链会问要不要用浏览器打开,
文内脚注(#fn-1)留在本章处理。
进度 — 关书时自动存 CFI;重开精确回到原处(不是「大概第 60%」)。
划线 / 笔记 / 书签 — 选中正文出菜单,五色高亮;一条高亮可以再挂笔记;
书签不依赖选中。侧栏列出本书全部标注,按章节、按颜色筛,点一条跳回去。
划过的线是能点的 — 点一下已有高亮,它进入选中态并浮出一条操作条:
你想做的事 · 怎么做
改颜色 · 点操作条上的五个色块,立刻生效、立刻存盘
写笔记 · 点「笔记」写到原来的锚点上,锚点不会丢
拿这段去问 AI · 点「问 AI」,这段原文自动带进提问面板
复制这段 · 双击高亮,整条文字变成系统选区,直接 Ctrl/⌘+C
删掉 · 点「删除」;5 秒内还能点提示条上的「撤销」找回
键盘党 · 高亮可 Tab 聚焦:Enter 选中整条、Delete 删除、Esc 收起
不会越叠越深 — 同一段文字不允许划第二次:再圈一段和已有高亮重叠的文字时不会新建,
而是把已有那条亮出来告诉你「这段已经划过线了」。老数据如果已经叠了,渲染时也只画一层,
同一块地方永远只有一个颜色。
圈住一段话,问问 AI
读到一段没读懂的话,用鼠标圈住它,浮出来的小菜单里点 问 AI,在底部输入框里写下你的问题(Enter 发送)。回答会直接显示在下面,同时带着这段原文的位置存进这本书的 qa.jsonl —— 下次翻回来还能找到它问过什么。
- 回答只用这段原文和通识,不确定就说不确定,不编;
- 小说里会刻意不剧透后面的情节;
- 这个功能走宿主自己的子代理机制,所以你得先在 DSH 里选一个模型。没选模型时它会告诉你「还没选模型」,而不是默默失败。
问过的话找得回来 — 阅读视图工具栏有个「问答」入口,打开就是这本书的全部问答记录:
按时间倒序列出你问过什么、它答了什么、问的是哪一段原文、在第几章。顶部有搜索框,
点任意一条就跳回那段原文所在的位置,接着问也行(同一个锚点上续问,答案不会串)。
记录存在这本书自己的 qa.jsonl 里,不上传。
它凭什么可信
读完上一节你会问:凭什么说「重开回到原处」是精确的?这里把三件容易被做错的事列出来,
因为它们决定了你的笔记三年后还在不在原位。
一、高亮不碰正文
划线最自然的做法是往正文里插 <span class="hl">。dsh-Reading 不这么做,
因为插一个元素就会改变 CFI 的编号基准:
<p id="p">abcdef</p> 选 1~5 → epubcfi(/4/2[p],/1:1,/1:5) <p id="p">abc<span></span>def</p> → 同一个选区变成 /1:1,/1:2
文本一个字没变,锚点全错。所以高亮画在覆盖层上:用 getClientRects() 拿到每段文字的矩形,
在父窗口的绝对定位层里画色块。正文 DOM 一个节点都不动 —— 这条有测试守着
(test/dom_immutability_test.mjs:加 50 条标注前后正文 DOM 结构指纹必须相同)。
二、CFI 计算强制带 filter
即使不碰正文,本项目自己也会往文档里注入东西(分页用的 <style>)。
所以所有 CFI 调用都强制带一个 filter:命中 data-dsh-reading-injected 的节点返回
FILTER_REJECT,整棵子树不参与索引。
(设计稿原本写的是 FILTER_SKIP。实测 vendor 对 SKIP 的处理前后不一致 ——
索引展开时把子节点并进父列表、生成路径时又把该节点那一段丢掉 —— 会算出一份
解回错位置的 CFI;改用 REJECT 后两边自洽。细节见 docs/07-审计修复记录.md 的 F7。)
这条不是可选项。client/src/cfi.js 的导出函数签名里没有 filter 参数 ——
想漏传也传不了。
三、校验分三层
foliate-js 的 parse() 很宽容:8 种明显畸形的 CFI 里有 5 种能「解析成功」,
其中两种最危险 —— 起止倒置(/1:8,/1:3)会静默折叠成零宽高亮,
跨文档(/6/4!/4/2)会落到 #document 上返回空文本。两者都不报错。
只靠 try/catch 挡不住。所以校验分三层:
层 · 检查 · 失败码
L1 语法 · parse() 不抛 + 恰好一个 ! · cfi_parse_failed
L2 语义 · 能解析成 Range + 起止不倒置 + 不跨文档 + highlight 不零宽 · `cfi_resolve
…(truncated)
以上内容来自 npm 上的 README,我们只做格式化,不改内容。
版本
npm 上共发布:6,最后更新 2026-11-03