dsh-reading-companion
DeepSeek Harness(DSH)的本地 TXT 阅读器 + 不剧透的 AI 陪读。 导入本地 TXT、自动生成目录、按章阅读并记住进度;边读边和 AI 聊感想——而它只知道你读过的部分。选中的「原文摘抄 — 我的感想 — AI 回应」会沉淀成结构化 Markdown 笔记,连同 AI 的「背景认识」一起导出到 Obsidian 之类的笔记库。
本地 TXT → 自动目录 → 正文阅读 → 进度持久化 → 绑定会话陪读
→ 三层防剧透 → 摘抄笔记 + 自动 tag → 背景认识增量补齐 → 导出到笔记库
书架与聊天窗口:左侧会话里陪读 AI 在聊感想,右侧书架列出导入的书与阅读进度
怎么用 · 导出的笔记 · 背景认识 · 防剧透 · 安装 · 数据目录 · 开发
更新说明
v2.0.4
- 发笔记不再空手而归:缺口超过阈值时,先自动补完开头 30 章,剩下的提示你去面板手动补。
- 进度跟着「你正在看的那一章」:修掉"跳章后不滚动 → 缺口算错、闸门不弹、按钮变灰"这一串问题;
面板同时显示「读到第 N 章 · 记忆到第 M 章」。
- 笔记列表改成真翻页:每页 10 条、能往回翻;每条笔记加了「回到第 N 章」。
- 新增「人物」卡:背景认识里"只含你读到部分"的按人视图。
- 导出改进:每本书一个文件夹;"压缩前"一代一个文件,不再出现重复的那一份。
更早的版本、每条改动的原因与实测数据都在 docs/design-v1.md 的修订块里。
缘起
本插件最初是为了更好地阅读某本小说而设计。
它是什么
它把一本书装进 DSH 的右侧栏,让「读」和「聊」在同一屏发生:左边是你的会话,右边是书。
| 特性 | 你会得到什么 |
|---|
| 纯本地 | 原书、笔记、AI 的理解都在你自己的磁盘上;没有账号、没有云、没有书源 |
| 不剧透 | AI 只能看到你读到的部分,三层机制保证(见 防剧透) |
| 读长篇不心疼额度 | 它读的是随进度增量补齐的「背景认识」,不是每章一次调用;一千多章也能一路读下去 |
| 笔记是成品 | 摘抄 + 你的感想 + AI 回应,带章节与 tag;可分页浏览,并能一键跳回对应章节 |
| 导出是干净的 | 笔记增量追加(不覆盖你在笔记库里的批注)、背景整份快照;文件名自带书名 |
怎么用
四个地方,各管一件事。
① 书架:导入、绑定、分类
把 TXT 丢进 $DSH_HOME/dsh-reading-companion/inbox/ 点「扫描导入目录」,或直接粘一个绝对路径。
每本书显示章数、体积、编码与阅读进度;点「跳过去」进正文,也可以先选个分类、绑定一个会话。
② 正文:选中一段,点「记笔记」
顶部是上一章 / 下一章 / 设置 / 笔记 / 字体(Aa)。在正文里选中一段,浮动条会自动弹出
「已选 N 字」与「记笔记」——点它会带着这段原文与该章节号进入笔记页。
正文页:选中一段后自动弹出「已选 46 字」与「记笔记」按钮
③ 笔记页:摘抄 → 感想 → AI 回应
四段式:原文摘抄(自动填)、我的感想、tag(按感想里的词确定性打分,可自己加)、
AI 回应(可选,留空就不落盘)。按钮依次是「发到会话去聊」「抓取选中文字作回应」「保存草稿」,
以及落盘用的「写入笔记」与「导出背景与全部笔记」。换笔记存放位置也在这一页。
笔记页:笔记保存位置、原文摘抄、我的感想、tag、AI 回应,以及发到会话/抓取回应/保存草稿/写入笔记/导出按钮
附:给某个情节生成一张插图(可选)
不用改插件、也不用装东西——在会话里说就行。
- 先聊清楚,再单独发一句生图请求。 先确认它理解对了那个瞬间,再让它画。
- 情节它本来就有。 发笔记时当前章全文已经在它的上下文里;要画更早的章,加一句定位或贴两句原文。
- 长相它没有。 小说很少写发色、服饰、年龄。一次锁定、之后复用:写「书友设定」(
persona.md)
最稳(那节按你写的原样注入,吃的不是背景那份预算);写进 background.md 的人物条目也行,
但没有章号的条目在预算不够时最先被丢。
- 别说"你去读人物卡"。 那张卡是给你看的视图,它看不到;它的人物信息来自
background.md。
想让它抓住谁,直接点名字。
再加一句保险:"以本章原文为准,不要自己加没写过的设定"。能不能真的生图取决于你那条模型路由,插件不参与。
④ 设置(「陪读模式」页):绑定、人设、预览、记忆
右侧栏「+」里选「陪读模式」:绑定会话、写「书友设定」(你想要的口吻与关注点)、
点「查看 AI 现在能看到什么」逐字复核注入给模型的内容、看背景认识记住到第几章、
以及这本书的讨论时间线。
设置页:陪读会话绑定、书友设定、AI 视角预览、背景认识(记忆)与人物卡、讨论历史
防剧透:三层
| 层 | 强度 | 管什么 |
|---|
| 提示词守则 | 常驻,不受任何开关影响 | 不主动说后续、不猜、分清自己知道与不知道 |
路径闸(spoilerGate) | 硬保证 | 参数指向本书 content.txt / source.txt / chapters.json 的调用一律拒绝,与会话归属无关 |
联网闸(webGate) | 启发式 / 可关 | 见下 |
它每轮实际拿到的只有三样:本章全文、上一章结尾、那份背景认识——外加你贴过去的摘抄。
你还没读到的地方,它字面上拿不到:路径闸连"模型自己想办法去读文件"这条路都堵了(../ 之类的绕过也挡,有专测)。
想亲眼验证就点设置页的「查看 AI 现在能看到什么」。
联网闸是启发式:扫工具参数里有没有书名、人物名、"结局/剧透"这类词,能挡住无心之失,
挡不住刻意查询——这一点写在守则里,也写在 docs/design-v1.md 里,不装成"绝对防得住"。
导出的笔记长什么样
点「导出背景与全部笔记」之后,落到 <你指定的导出目录>/陪读导出_<书名>/,文件名是 <书名>-笔记.md:
一份带章节与 tag 的摘抄本,可以直接丢进 Obsidian。
导出到 Obsidian 的读书笔记:章节标题、tag、原文摘抄、我的感想、AI 回应
- 只追加,绝不覆盖。 你在笔记库里写的批注、加的双链,重复导出一个字都不会被碰。
- 绝不往陌生文件里写。 每个导出文件头部有一条
<!-- drc-export book=… --> 标记;目标属于别的书、
或者压根没有标记(那是你自己写的文件),一律拒绝并报错。
- 手写的、没有 id 的笔记块不导出,并会明说几条。
背景认识(记忆)
它是陪读 AI 对这本书的理解,一份随进度只增不减的 Markdown:
## 人物关系
- 甲 → 乙:救下之后收为徒(第 3 章)
## 人物
### 甲
- `第2章` 借六岁女童之身重生,处境贫苦
- `第29章` 潭边第一次无法再掩饰心意
## 世界观 / ## 文风 / ## 前文脉络 / ## 通用概念(兜底)
- 写入
background.md,你可以直接打开读、也可以改——AI 记错了,改一行就是纠正。
- 面板里另有一份只读视图「人物」卡:只含你读到的部分,按主体归堆、标注最新章号。
它的判定就是分区:只有
## 人物 一节里的主体出卡。想让某个名字进出这张列表,改 background.md 的归属即可。
- 缺口大时不硬补:从目录跳到很靠后的一章(缺口超过 50 章)会先拦一下,让你选
「这些我都读过 / 只记最近这一段 / 先不补」——把没读到的章节写进记忆是不可逆的。
而发笔记那一路不会空手而归:它会自动把开头 30 章跑完,再告诉你去面板补剩下的。
- 压缩是唯一会删内容的一步,所以要过四条硬校验(保名 / 保主体 / 保号 / 真的变小),
任何一条不过就整批丢弃、文件一字不动;每次压缩前还会留一代带时间戳的备份,一份不删,
导出时一代不漏地跟出去。
导出到 Obsidian 的背景认识:人物关系与人物条目,每条都带章号
想要"最完整的那一版":把历代并起来。 每一代都是完整快照(不是增量),压缩只做删除与合并,
所以越早的那一代覆盖的章更少、但每条更细——"历代 + 当前"的并集才是最详细的那份。
但压缩后的条目没有 id、没有稳定标题,章号区间也可能被改写,"哪条对应哪条"无法可靠判定,
所以插件刻意不做自动合并。推荐做法:读完之后,把陪读文件夹(或导出的那组
-背景-压缩前-<时间戳>.md)交给 AI 或别的工具合并,并且保留原文件。可以直接用这段话:
把这本书的这几份背景认识合并成一份最详细的版本。输入:background.md(最新,已压缩)
与 background.bak.*.md(历代压缩前快照,越早的通常越详细)。规则:
① 以并集为目标——任何一代里出现过的条目都要保留;
② 同一主体下按章号对齐;同一章号有多个版本时取更详细的那一条;
③ 不要发明输入里没有的内容,也不要按你的小说知识补充;
④ 两条冲突时并存并标注;
⑤ 输出到新文件,一个输入文件都不要改;
⑥ 最后列出:补回了多少条、多少条无法对应、哪些章号有冲突。
安装
[!IMPORTANT]
前置:dsh-better-sidebar ≥ 0.19.0(本仓库在 0.19.1 上验证)。
本插件自己不画侧边栏——它只是往别人提供的右侧栏里注册一个页签,那个接口(sidebarRightTabs)
由它发布。缺了它的表现很坑:右侧栏「+」里看不到「陪读模式」,而控制台没有任何报错。
另需 DSH ≥ 0.1.5-rc.2、Node ≥ 22.19。
先确认你的 profile 名
| 你用的面 | profile 名 | profile 目录 |
|---|
| DSH Desktop | desktop | $DSH_HOME/profiles/desktop |
DSH Web(dsh web) | web | $DSH_HOME/profiles/web |
$DSH_HOME 默认是 ~/.dsh(Windows:C:\Users\<你>\.dsh)。别把 --profile desktop 抄给用 Web 的人:
内置模板只有 acp / web / headless / sdk / sdk-minimal,desktop 是 DSH Desktop 自建的。
装(推荐让 DSH 自己装)
把下面整段复制到 DSH 对话框里发出去,它会自己找 profile、检查并补齐前置、装好、核对 manifest:
请帮我把 DSH 插件 dsh-reading-companion 装进我当前的 profile。
1. 先确定 profile 目录:我用的是 DSH Desktop,profile 名应该是 desktop;如果我的环境实际属于别的面,
请告诉我正确的 profile 名再继续。目录 = $DSH_HOME/profiles/<profile 名>,$DSH_HOME 默认 ~/.dsh。
确认该目录下确实有 package.json 和 cordis.yml。
2. 检查前置插件 dsh-better-sidebar(需要 >= 0.19.0)。先看 profile 的 package.json 里
dependencies 与 dsh.profile.bundles 有没有它。没有就先装,并告诉我最终版本号:
dsh plugin --profile <profile 名> add dsh-better-sidebar
这一步不能跳过:本插件的界面完全依赖它发布的 sidebarRightTabs 服务,缺了它右侧栏不会出现
「陪读模式」,而且不会报任何错。
3. 装本插件:
dsh plugin --profile <profile 名> add "github:xling001/dsh-reading-companion"
(这条命令会把包加进 dependencies,并在 pnpm 跑完后自动把 dsh-reading-companion 补进
package.json 的 dsh.profile.bundles。)
4. 装完核对 profile 的 package.json 这两处:dependencies 里有 "dsh-reading-companion"、
dsh.profile.bundles 里有 "dsh-reading-companion"。缺哪条补哪条。
不要改动其它插件的依赖,也不要调换 bundles 里已有条目的顺序。
5. 最后告诉我需要重启 DSH Desktop,以及重启后怎么验证装好了。
或者:命令行 / 手工 / 本地开发
# 前置(没装过才需要)
dsh plugin --profile desktop add dsh-better-sidebar # Web 换成 --profile web
# DSH Desktop / DSH Web
dsh plugin --profile desktop add "github:xling001/dsh-reading-companion"
dsh plugin --profile web add "github:xling001/dsh-reading-companion"
dsh plugin 只做一件事:把剩余参数转发给 profile 目录里的 pnpm。所以你不用手动改 bundles
——pnpm 结束后,DSH 会把「声明了 dsh.bundle 的依赖」自动补进去。从 GitHub 装时若 pnpm 提示构建脚本
被拦下,把它打印的 key 加到 $DSH_HOME/profiles/<profile>/pnpm-workspace.yaml 的 allowBuilds 下重跑一次
(本插件没有构建步骤,正常不会遇到)。
手工等价操作:把本包加进 profile 的 dependencies("dsh-reading-companion": "file:/abs/path/…"),
并把包名加进 dsh.profile.bundles,然后在该目录跑 pnpm/npm 安装。Windows 路径注意:
file: 依赖请用正斜杠。
本地开发(改完即生效)用仓库自带脚本——它只碰自己那一个键,并在 profile 的 node_modules
里建一个目录联接指向本仓库,所以不需要跑 pnpm install,也不会打扰 profile 里已有的其它插件:
node scripts/link-into-profile.mjs --profile desktop --dry-run # 先看将要做什么
node scripts/link-into-profile.mjs --profile desktop # 实际写入
node scripts/link-into-profile.mjs --profile desktop --unlink # 完全回滚
⚠️ 装完必须重启 DSH Desktop
dsh.profile.bundles 只在启动时读取一次。patchReload: "live" 只覆盖 cordis.patch.yml 的改动,
覆盖不了"新增一个 bundle"。刷新页面不够,要重启应用(dsh web 同理:重启那个进程)。
验证
- 打开任意会话,点右侧栏的「+」;
- 列表里应出现「陪读模式」(一本摊开的书的图标);
- 点开进入书架视图。
看不到时按顺序查:前置装了没?(这一步最容易被漏)→ 重启了没?(右侧栏选择器的条目
完全由插件注册的 guide 数组构建,看不到就是客户端半边没挂上)→ 都没有看控制台报错,请开 issue。
接着导一本书、读一章、记一条笔记。最短全流程与发版前的真机回归清单在 docs/manual-testing.md。
关闭与卸载
本插件是纯加法的:不替换任何宿主自带的行,也不接管既有服务。
- 临时关闭:把
dsh-reading-companion 从 profile 的 dsh.profile.bundles 里删掉,改完重启。
- 彻底卸载:
dsh plugin --profile desktop remove dsh-reading-companion(Web 换成 --profile web),
或用 node scripts/link-into-profile.mjs --unlink。
- 数据不会被卸载删除:书库与笔记都在独立目录里,删插件不删书。
数据目录
人可读的东西跟着会话工作区走,大文件留在插件目录。
<会话工作区>/陪读_<书名>/ # ★ 你的笔记在这里
notes.md # 结构化读书笔记(只追加,永不重写)
background.md # 陪读 AI 的背景认识(条目只增不减)
persona.md # 你写给 AI 的「书友设定」
background.bak.<时间戳>.md # 每次压缩前留一代,一份不删
README.md / .dsh-reading-companion.json # 自动生成的说明 / 认领标记
$DSH_HOME/dsh-reading-companion/ # 默认;可用 cordis.patch.yml 的 storageDir 覆盖
inbox/ # 把 TXT 丢这里,点「扫描导入」
library.json / bindings.json / drafts.json / categories.json
books/<bookId>/
meta.json # 书名/编码/字数/章节数/解析告警
source.txt # 原书原始字节(只读,永不改写)—— MB 级
content.txt # 解码并归一化换行后的 UTF-8 全文 —— MB 级
chapters.json # 章节索引(标题 + 精确的字符/字节区间)
discussions.jsonl # 讨论时间线(每行一条摘要)
notes.md / background.md / persona.md # ← 迁移期间的安全网副本
拿不到工作区时(还没绑定、或路径失效)退回插件目录,笔记照样写得进去,「笔记」页会把实际路径
与回落原因摊给你看,并给一个「重新检测位置」。两本书绝不会写进同一份笔记:每本书一个文件夹,
同一工作区里两本不同的书同名时后来者变成 陪读_<书名>_<bookId 前 6 位>(有专测钉住)。
迁移是复制,不是移动——老文件原样保留作安全网,你确认没问题后可以自己删。
bookId = 源文件 sha256 的前 16 位,所以同一份文件重复导入是幂等的:命中已有记录、不重复落盘、
更不会覆盖你写过的笔记。
正文字体
右上角「Aa」展开:字号(13–30px)、行距(1.2–2.6)、字体(宋体 / 黑体 / 楷体 / 等宽)。
偏好存在浏览器本地(localStorage),切章、重开面板都不变。字体栈只列系统自带字体。
三个刻意的排版选择:行宽用 em(36em,跟着字号走,所以调字号时每行字数不变)、
章节标题也用 em(写死 px 的话字号调到 20px 后标题会比正文小,层级反向)、标题后第一段保留 2em 缩进。
隐私
- 原书 TXT、章节索引、笔记 md 全程留在本地,不上传任何服务器。
- 只有你主动发送感想时,被裁切过的那段正文才会随对话进入模型请求——裁切范围是「前文 + 本章已读」,
不包含后续剧情。
- 导出只写到你指定的那个目录,也只在你点了按钮之后才写;导出不会改动陪读文件夹里的任何东西。
- ⚠️ 导入接口是一条本地文件读取面。
POST /library/import 接受一个绝对路径并把它读进书库——
插件自己没有鉴权,这一条完全依赖宿主的渲染器令牌门。想把可导入范围收窄,在 cordis.patch.yml
里配 importRoots(非空时只接受落在这些根目录内的路径;判定走真实路径,用链接绕不过去)。
开发
没有构建步骤、没有运行时依赖。 浏览器半边是宿主模块加载器认的惰性 CJS 信封
(window.__ModuleLoader__.load({ id, factory })),唯一外部依赖是壳提供的 require('react');
宿主半边只用 node: 内置模块。所以 lib/ 就是源码,改完重启 DSH Desktop 即生效。
npm test # 跑全部测试
npm run test:no-isolation # 受限沙箱里(无法 spawn 子进程)用这个
node scripts/reindex-books.mjs # 预演:让书架里已有的书吃到新切分规则
node scripts/reindex-books.mjs --apply # 真的落盘(先把要改的文件备份到 backups/)
node scripts/clean-background-note.mjs --file <background.md 路径> # 清理注释残留(默认预览)
切分规则的改动不会自动作用于已导入的书(导入是幂等的),所以老书要么删掉重导(丢笔记、丢进度),
要么就地重切。代码结构、测试清单、以及客户端测试替身的盲区都在 CONTRIBUTING.md。
后续计划
AI 页边批注。 给陪读 AI 一个 annotate(原文, 批注) 工具,让它也能给某段原文写批注。
一条硬要求:入库前必须校验「那段原文真的是本章子串」——否则 AI 编一段不存在的原文,批注就挂在空气上。
贡献者
| 贡献者 | 负责 |
|---|
| xling001 | 功能设计、方案取舍、真机验证 |
| AI(DSH 内的编码 agent) | 代码实现、测试、文档 |
本仓库的代码主要由 AI 编写。 人类作者负责提出要解决什么问题、在几个方案之间做选择、以及在真机上发现"哪里不对"。
与同类插件的区别,以及参考了哪些插件
同类里定位最接近的是 dsh-reader(用 DOM 选择器
冒充插槽,在本机 DSH 上中央列会被清空,且没有任何 AI 机制)与
dsh-novel-forge(创作工具台,本项目只读)。
本项目只往官方插槽注册,页签落在右侧栏、不接管中央列,因此与
dsh-tavern 这类插件共存无冲突。
借鉴过的(出处都在源码注释里,grep dsh- 就能找到):dsh-reader 的编码探测顺序、章节正则基线与
"inbox 扫描导入"形态,以及它几个真实故障的反面教训;dsh-novel-forge 的"把正文放进右侧栏页签"路线;
dsh-better-sidebar 的页签注册接口;dsh-tavern 的路径闸做法与共存矩阵;dsh-adaptive-context
的一条踩坑形状(压缩失败不该阻塞补齐)。只做过定位对比、没有借鉴具体机制的:dsh-novel-solo、dsh-talebook-plugin。
许可
MIT