본문으로 건너뛰기
X

dsh-reading-companion

xling001/dsh-reading-companion

Local TXT reader in a right-sidebar tab plus a spoiler-safe AI reading companion: the model only sees what you have already read — the current chapter, the tail of the previous chapter, and a per-book background file it grows as you go. Excerpts, thoughts and the companion replies become structured Markdown notes exportable to Obsidian; raw text unlocks only for a book you mark as finished.

설치

dsh plugin --profile web add github:xling001/dsh-reading-companion

README

dsh-reading-companion

License: MIT dsh-plugin DeepSeek Harness

AI 陪伴 + 精读小说 —— Novel companion。 这是 DeepSeek Harness(DSH)的本地 TXT 阅读器 + 不剧透的 AI 陪读插件:把一本书装进右侧栏,让「读」和「聊」在同一屏发生 —— 你在正文里划一段、写下想法,它接着聊;而它只读到你读到的地方。

  • 读什么:本地 TXT(自动探测编码、自动切章;无标题时降级为固定块),原书、笔记、AI 的理解全在你自己磁盘上 —— 无账号、无云、无书源
  • 怎么陪:陪读 AI 的视野 = 本章全文 + 上一章结尾 + 随进度增量补齐的「背景认识」,不含你未读的部分(想看它到底收到了什么,见下面「防剧透」那节)
  • 留下什么:摘抄 → 我的感想 → AI 回应,写成结构化 Markdown 笔记,可直接导出进 Obsidian 之类的笔记库
  • 怎么装:DSH 插件(Cordis bundle),零运行时依赖、零构建步骤 —— lib/ 就是源码

本地 TXT → 自动目录 → 正文阅读 → 进度持久化 → 绑定会话陪读
→ 三层防剧透 → 摘抄笔记 + 自动 tag → 背景认识增量补齐 → 导出到笔记库

书架与聊天窗口:左侧会话里陪读 AI 在聊读后感想,右侧「本地书架」面板列出导入的书、分组、分类与绑定状态

更新说明 · 功能总览 · 怎么用 · 防剧透 · 安装 · 配置参考 · 数据目录 · 开发


更新说明

这里的版本号是推送给使用者的版本。设计文档里另有一套内部迭代号(同一段工作可能被拆成 好几轮),两套号是刻意分开的 —— 想知道每一轮到底改了什么、为什么改,去 docs/design-v1.md。

2.2.0-2.2.3:

  • 页签名「陪读模式」→「本地书架」;插件别名写作 Novel companion。⚠️ 仓库地址、包名、页签 id 都没动 —— 改名会弄断已经装了它的人的更新路径。
  • 配色全部归还宿主:面板从前引用 12 个宿主的 CSS 变量名,而宿主一个都没有,它其实一直在用写死的颜色(两个多月没露馅,是因为"宿主写死、我们也写死")。现在强调色 / 次级文字 / 分割线 / 状态色 / 各种底与字体族都跟着主题与换肤插件走;选中文字的高亮也从浏览器默认的亮蓝换成按主题现算的暖灰(浓度 34%)。
  • 笔记更稳:新笔记自带「文本锚」(摘抄前后各 32 字 + 源文件指纹);重新切分书不再"有笔记就一律拒绝" —— 逐条核对,位置没变的放行、挪了位置的报出新章号、对不上的单独列出并仍然拦下(核对只读,绝不改你的笔记)。顺带堵掉一个数据坑:笔记标记里出现 > 会让整条笔记落了盘却读不出来。
  • 收起与折叠:书架分类、人物卡都能折成一行;收起状态记住(人物卡默认折叠)。
  • 提示条改成浮动胶囊(观感对标另一款轻量 EPUB 阅读器,MIT):底部居中、毛玻璃、点击即消、短提示 3 秒;长提示不自动消失("导入结果"那类信息不该被收走)。
  • 界面文字不再能"拖选"(输入框、正文、笔记三段、只读输出仍可选 —— 正文那条最关键:选不中正文,"选中一段去记笔记"就整条废了)。
  • 设置页更清爽:人物卡独立成节(挪到讨论历史上方,默认折叠);「AI 视角预览」整块去掉(它只是别处状态的视图,需要时可走只读接口 GET …/books/<bookId>/context);讨论历史一次交互只记一条(从前"写笔记 + 发去聊 + 抓回回应"会各记一条)。
  • 顺带:清掉一处死代码、清约 285 MB 本地冗余,测试不再在 test/.tmp 留残留。
  • 守则加厚了三处(都只改提示词,不动机制):① 元剧透 —— "后面有反转""熬过这段就好""以后看到 X 留意"这类话也算剧透(它们不点明情节,却替你制造了期待);② 引文只能来自原文或你贴的片段(不许凭记忆引 —— 那既可能背错,也可能把后面才出现的句子说出来);③ 用了二手来源就第一句声明(书评 / 百科 / 它自己的记忆都算),而且永远不用二手来源纠正你读到的内容。
  • 记忆有缺口时的说法分清了:它的记忆停在第 10 章、你的进度在第 30 章,你问第 20 章的事 —— 它会如实说"这一段我还没纳入记忆,暂时答不准"并告诉你去点「补齐前文记忆」,而不是含糊地说"这个我不能说"(那句听起来像剧透,其实只是它还不知道)。
  • 人设框的例文提示补成五个方向(口吻与篇幅 / 称呼 / 关注点 / 聊多深 / 卡住时),照着写或完全不写都行;默认仍然是空白。

2.1.4:

  • 面板配色第一次真正跟随你的主题与换肤插件 —— 就是上面那条"归还宿主"的起点。
  • 新增「纸张感」(默认关,Aa 条里「纸− / 纸+」0–30%):阅读区整块底色掺进暖色,浅色掺奶油、深色掺暖炭,颜色由宿主底色现算。
  • 导入更顺手:收件箱地址以宿主实际使用的为准(顺带修好一个从 v1.0 起就"改了没反应"的配置 inboxDir);导入成功后自动归档到 .imported/(只搬走、不删除);多了「复制路径」「打开导入目录」两个入口。
  • 「导入说明」可以重看了(有问题才出现,正常导入不占地方)。
  • 修掉两处真问题:装新插件后书架里的「跳过去」可能永久失效;正文出现竖直滚动条时整体偏左。

更早的版本、每条改动的原因与实测数据都在 docs/design-v1.md 的修订块里。

缘起

本插件最初是为了更好地阅读某本小说而设计。


功能总览

能力说明入口
本地阅读导入本地 TXT(BOM → 严格 UTF-8 → GB18030 依次探测;按标题正则切章,无标题时降级为固定块并给出解析告警)。目录、正文、上一章/下一章、字体(字号 / 行距 / 字体族);阅读进度按书持久化。目录按卷分组并折叠(只展开当前卷、筛选时全开,卷标题带起止章号与章数)——1400 章的书不必一次铺出七千个元素书架 / 正文页
读者坐标全插件只有一个坐标:你读到第几章。投喂窗口、记忆缺口、跳读闸、倒退过滤、讨论注入全部由它派生——所以它绝不会"顺手"知道更多—
不剧透陪读三层:提示词守则(常驻,不受开关影响)+ 路径闸(硬拒绝指向本书 content.txt / source.txt / chapters.json 的调用,与会话归属无关,../ 绕过也挡)+ 联网闸(默认 block-all;block-book 是启发式,明说挡不住刻意查询)。注入内容可用只读接口 GET …/books/<bookId>/context 逐字复核设置页
摘抄笔记正文里选中一段即弹出「记笔记」,带这段原文与该章节号进入四段式:原文摘抄 / 我的感想 / tag(按关键词确定性打分,零模型调用)/ AI 回应(留空则不落盘)。正文页还会显示「本章你记过 N 条」——点开是每条一行摘要(感想前 30 字)与「跳到这一段」,点它回到原文那一段(没选中原文的显示「整章」);长文不在正文里铺开,回笔记页读(那里可「展开全文」)。笔记列表分页浏览(每页 10 条、可往回翻),每条可一键跳回对应章节正文页 / 笔记页
背景认识(记忆)随进度增量补齐的 background.md:六分区(人物关系 / 人物 / 世界观 / 文风 / 前文脉络 / 通用概念)+ ### 主体 分组 + 每条带章号 + 只增不减。缺口超过阈值时先问再补,发笔记那一路自动打底开头 30 章;压缩过四条硬校验并留历代备份设置页 / background.md
人物卡「只含你读到部分」的按人只读视图,标注最新章号。判定就是分区归属:只有 ## 人物 里的主体出卡,改文件即可增删设置页
导出笔记增量追加(不覆盖你在笔记库写的批注与双链)、背景整份快照、「压缩前」一代一个文件;文件名自带书名、每本书一个文件夹;拒绝写进没有标记的陌生文件设置页
书友设定 / 讨论历史persona.md 管口吻与关注点(与守则一起生效,不覆盖硬底线);讨论时间线保存在书目录里,可回顾设置页

怎么用

四个地方,各管一件事。

① 书架:导入、绑定、分类

把 TXT 丢进 $DSH_HOME/dsh-reading-companion/inbox/ 点「扫描导入目录」,或直接粘一个绝对路径。 每本书显示章数、体积、编码与阅读进度;点「跳过去」进正文,也可以先选个分类、绑定一个会话。

② 正文:选中一段,点「记笔记」

顶部是上一章 / 下一章 / 设置 / 笔记 / 字体(Aa)。在正文里选中一段,浮动条会自动弹出 「已选 N 字」与「记笔记」——点它会带着这段原文与该章节号进入笔记页。

③ 笔记页:摘抄 → 感想 → AI 回应

四段式:原文摘抄(自动填)、我的感想、tag(按感想里的词确定性打分,可自己加)、 AI 回应(可选,留空就不落盘)。按钮分两行:① 发到会话去聊 / ② 抓取选中文字作回应, 然后是落盘用的「写入笔记」(主按钮)与暂存用的「保存草稿」;导出在「设置」页 (那一页还能记住导出目录)。换笔记存放位置也在这一页。

笔记页:笔记保存位置、原文摘抄、我的感想、tag、AI 回应,以及「发到会话去聊 / 抓取选中文字作回应」「写入笔记 / 保存草稿」与笔记 / 草稿 / 回收站三个分页选项卡

④ 设置(「本地书架」页):绑定、人设、记忆

右侧栏「+」里选「本地书架」:绑定会话、写「书友设定」(你想要的口吻与关注点)、 看背景认识记住到第几章、翻人物卡(只含你读到的部分), 以及这本书的讨论时间线。

设置(陪读)页:陪读会话绑定与解除、书友设定(写给你自己的 AI 的口吻)、人物卡,以及背景认识(记忆)记住到第几章


防剧透:三层

层强度管什么
提示词守则常驻,不受任何开关影响不主动说后续 ——包括"制造期待"式的元剧透("后面有反转"、"熬过这段就好"、"以后看到 X 留意"、"我先不说");引文只能来自原文(不许凭记忆引,那可能把后文引出来);分清事实 / 引语 / 推断;用了二手来源(书评 / 百科 / 它自己的记忆)就第一句声明,且读者永远优先于二手来源
路径闸(spoilerGate)硬保证(唯一例外见下)参数指向本书 content.txt / source.txt / chapters.json 的调用一律拒绝,与会话归属无关。⚠️ 唯一例外:你在面板里声明「这本书已读完」之后,这一本的原始文本对你放开(界面常驻显示,可一键收回)
联网闸(webGate)启发式 / 可关见下

它每轮实际拿到的只有三样:本章全文、上一章结尾、那份背景认识——外加你贴过去的摘抄。 你还没读到的地方,它字面上拿不到:路径闸连"模型自己想办法去读文件"这条路都堵了(../ 之类的绕过也挡,有专测)。 读完一本书之后,你可以在面板里标记「已读完」解锁它:那只放开这一本的原文("你问,它才读得到"),不影响每轮自动投喂的内容,而且可以一键收回。 想亲眼复核它到底收到了什么:注入的内容由插件的只读接口原样给出 —— GET /dsh-reading-companion/api/books/<bookId>/context(面板里不再放这个入口, 因为它只是「别处状态的视图」,摆一节在那里会让人以为它可以单独重建)。

联网闸是启发式:扫工具参数里有没有书名、人物名、"结局/剧透"这类词,能挡住无心之失, 挡不住刻意查询——这一点写在守则里,也写在 docs/design-v1.md 里,不装成"绝对防得住"。

导出到笔记库

在「设置」页点「导出背景与全部笔记」之后,默认落到这本书所绑会话的工作区根下的 陪读导出_<书名>/,文件名是 <书名>-笔记.md;在设置页里填过一次导出目录就落到那里 (还没绑定会话、又没填目录时它会让你先指定一个 —— 不会乱猜一个位置写进去)。

它就是普通的 Markdown:用任何笔记库工具打开、编辑、提交到版本管理都行。

  • 只追加,绝不覆盖。 你在笔记库里写的批注、加的双链,重复导出一个字都不会被碰。
  • 绝不往陌生文件里写。 每个导出文件头部有一条 <!-- drc-export book=… --> 标记;目标属于别的书、 或者压根没有标记(那是你自己写的文件),一律拒绝并报错。
  • 手写的、没有 id 的笔记块不导出,并会明说几条。

背景认识(记忆)

同一人物被写成两个 ### 时,用工具合并: node scripts/merge-background-subjects.mjs --file <background.md> --merge '<被并掉的名字>=<留下的名字>' —— 默认只预览,加 --apply 才写,写前自动备份。压缩不会替你合并:它的「保主体」校验不许丢任何一个主体。

它是陪读 AI 对这本书的理解,一份随进度只增不减的 Markdown:

## 人物关系
- 甲 → 乙:救下之后收为徒(第 3 章)
## 人物
### 甲
- `第2章` 借六岁女童之身重生,处境贫苦
- `第29章` 潭边第一次无法再掩饰心意
## 世界观 / ## 文风 / ## 前文脉络 / ## 通用概念(兜底)
  • 写入 background.md,你可以直接打开读、也可以改——AI 记错了,改一行就是纠正。
  • 面板里另有一份只读视图「人物」卡:只含你读到的部分,按主体归堆、标注最新章号。 它的判定就是分区:只有 ## 人物 一节里的主体出卡。想让某个名字进出这张列表,改 background.md 的归属即可。
  • 缺口大时不硬补:从目录跳到很靠后的一章(缺口超过阈值)会先拦一下,让你选 「这些我都读过 / 只记最近这一段 / 先不补」——把没读到的章节写进记忆是不可逆的。 而发笔记那一路不会空手而归:它会自动把开头 30 章跑完,再告诉你去面板补剩下的。
  • 压缩是唯一会删内容的一步,所以要过四条硬校验(保名 / 保主体 / 保号 / 真的变小), 任何一条不过就整批丢弃、文件一字不动;每次压缩前还会留一代带时间戳的备份,一份不删, 导出时一代不漏地跟出去。

想要"最完整的那一版":把历代并起来。 每一代都是完整快照(不是增量),压缩只做删除与合并, 所以越早的那一代覆盖的章更少、但每条更细——"历代 + 当前"的并集才是最详细的那份。 但压缩后的条目没有 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(engines: ^22.19.0 || >=24.0.0)。

一键装(推荐让 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"
4. 装完核对 profile 的 package.json 这两处:dependencies 里有 "dsh-reading-companion"、
   dsh.profile.bundles 里有 "dsh-reading-companion"。缺哪条补哪条。
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"
你用的面profile 名profile 目录
DSH Desktopdesktop$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 plugin 只做一件事:把剩余参数转发给 profile 目录里的 pnpm。所以你不用手动改 bundles ——pnpm 结束后,DSH 会把「声明了 dsh.bundle 的依赖」自动补进去。从 GitHub 装时若 pnpm 提示构建脚本 被拦下,把它打印的 key 加到 $DSH_HOME/profiles/<profile>/pnpm-workspace.yaml 的 allowBuilds 下重跑一次 (本插件没有构建步骤,正常不会遇到)。

本地开发(改完即生效)用仓库自带脚本——它只碰自己那一个键,并在 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 同理:重启那个进程)。

验证

  1. 打开任意会话,点右侧栏的「+」;
  2. 列表里应出现「本地书架」(一本摊开的书的图标);
  3. 点开进入书架视图。

看不到时按顺序查:前置装了没?(这一步最容易被漏)→ 重启了没?(右侧栏选择器的条目 完全由插件注册的 guide 数组构建,看不到就是客户端半边没挂上)→ 都没有看控制台报错,请开 issue。

接着导一本书、读一章、记一条笔记。最短全流程与发版前的真机回归清单在 docs/manual-testing.md。

关闭与卸载

本插件是纯加法的:cordis.patch.yml 里只有一条 insert,不替换任何宿主自带行、不接管既有服务。

  • 临时关闭:把 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。
  • 数据不会被卸载删除:书库与笔记都在独立目录里,删插件不删书。

配置参考(全部字段与默认值 —— 需要时展开)

配置参考

配置写在 cordis.patch.yml 的那条 insert 里,任何字段都可在 profile 的 cordis.patch.yml 覆盖。 标注「运行时」的项,读者也能在面板里改,且面板优先于配置文件。

字段类型默认值说明
storageDirstring''书库与笔记根目录。留空是有意的:默认走宿主的 dshHomePath() 解析成 $DSH_HOME/dsh-reading-companion,这样 profile 迁移时书库跟着走
inboxDirstring'inbox'「扫描导入目录」扫的收件箱,相对 storageDir
fallbackBlockCharsnumber4000TXT 没有可用章节标题、降级为固定块时的块大小(字符)
importRootsstring[][]POST /library/import 的白名单。空 = 不限制(导入本来就是"从磁盘任意处读书"这个功能本身)。填了就只接受落在这些根目录内的路径,判定走真实路径,用链接绕不过去
exportDirstring''默认导出目录。空 = 落到这本书所绑会话的工作区根;面板里改过一次就写进 settings.json,优先级 settings > 此值 > 工作区根
spoilerGatebooleantrue路径闸:指向本书原始文本(content.txt / source.txt / chapters.json)的工具调用一律拒绝,与会话归属无关
webGate'block-all' | 'block-book' | 'off''block-all'联网闸强度(运行时可在面板改)。block-book 是启发式:放行联网,但拒绝看起来在问这本书的查询
window.currentChapterMode'full' | 'read-so-far''full'当前章给全文,还是只给到光标处
window.headAllowanceCharsnumber1500仅 read-so-far 用:至少给当前章开头这么多字符
window.previousChapterMode'tail' | 'full''tail'上一章给多少:只给结尾(在段落处切)还是整章
window.backgroundBudgetCharsnumber9000背景认识那段的上限;超了按优先级裁剪,面板会说明裁掉了什么
window.backgroundCoarseDegradebooleantrue降级第二档:主体整体被丢之前,先降成"### 主体 + 最近一条"
window.compactThresholdnumber0.85背景超过 backgroundBudgetChars × 此值 时,下一次补齐先压缩(设为 1 = 关闭自动压缩)
window.discussionLimitnumber8注入多少条讨论时间线
sample.budgetCharsnumber24000一次补齐调用的字符预算——它决定一次能闭合多大的缺口
sample.minPerChapternumber600默认形态下这是"均分额度的下限",同时决定一批能吞多少章:一批章数 ≈ budgetChars / minPerChapter(600 → 约 40 章)。⚠️ 不要设得比 maxPerChapter 高,否则这个下限会被上限吞掉
sample.maxPerChapternumber1200单章上限(重点章可拿到它的 emphasisFactor 倍)。批越窄,budgetChars ÷ 权重和 算出的额度越高,靠它放行
sample.lengthRationumber0可选形态:0 = 均分(默认);设 0.2 改为"按章节长度比例分配"(代价是短章被切得更狠,见 design-v1 §197)
sample.foundationChaptersnumber30只对第一次补齐生效的上限:第一次就厚读开头,而不是把预算摊到几百章
sample.emphasisChaptersnumber5开头前 N 章(以及每卷的卷首章)按 emphasisFactor 加权
sample.emphasisFactornumber3加权倍数
sample.jumpGateChaptersnumber50跳读闸阈值:一次补齐要闭合的缺口超过它就先问(回 409 与缺口范围,面板给三个选项)。设 0 关闭
sample.recentWindowChaptersnumber200选「只记最近这一段」时的窗口大小(调用时可临时改,这是默认值不是上限)
sample.recentMinPerChapternumber1200只给 recent 路径用的每章下限(比 minPerChapter 厚:那条路要的是"能聊这一章",不是"不致迷路")。⚠️ 必须 ≤ maxPerChapter,否则形同虚设
memoryTimeoutMsnumber120000一次补齐最多阻塞多久(插件内部另有中止定时器,不会永久挂住)

数据目录

人可读的东西跟着会话工作区走,大文件留在插件目录。

<会话工作区>/陪读_<书名>/          # ★ 你的笔记在这里
  notes.md                          # 结构化读书笔记(只追加,永不重写)
  background.md                     # 陪读 AI 的背景认识(条目只增不减)
  persona.md                        # 你写给 AI 的「书友设定」
  background.bak.<时间戳>.md        # 每次压缩前留一代,一份不删
  README.md / .dsh-reading-companion.json   # 自动生成的说明 / 认领标记
$DSH_HOME/dsh-reading-companion/      # 默认;可用 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 位,所以同一份文件重复导入是幂等的:命中已有记录、不重复落盘、 更不会覆盖你写过的笔记。


安全与隐私

要求实现
数据本地化原书 TXT、章节索引、笔记 md、背景认识全程留在本地,不上传任何服务器
只发该发的只有你主动发感想时,被裁切过的那段正文才随对话进入模型请求——裁切范围是「前文 + 本章已读」,不含后续剧情
导出可控只写到你指定的那个目录,也只在你点了按钮之后才写;不会改动陪读文件夹里的任何东西
不覆盖你的字导出只追加,并靠文件头部的 <!-- drc-export book=… --> 标记拒绝写进陌生文件
⚠️ 导入面要说清POST /library/import 接受一个绝对路径并把它读进书库——插件自己没有鉴权,这一条完全依赖宿主的渲染器令牌门。想收窄范围就配 importRoots(判定走真实路径)
无遥测本插件没有账号、没有云、没有书源,也不含任何遥测/行为分析代码
架构简介(代码结构与关键设计 —— 需要时展开)

架构简介

一切皆插件、零依赖、无构建。 宿主半边(Node)只用 node: 内置模块;浏览器半边是宿主模块加载器认的 手写惰性 CJS 信封(window.__ModuleLoader__.load({ id, factory })),唯一外部依赖是壳提供的 require('react')——所以 lib/ 就是源码,省掉了整条构建链与全部 devDependencies。

lib/
├── index.js                # 宿主入口:cordis 插件名、prefix 路由、服务发布、prompt 段落回调
├── client.js               # 浏览器半边(必须自包含):React 手写 h(),书架/正文/笔记/设置四个视图
└── host/
    ├── library.js          #   书架:导入、编码探测、切章、进度、绑定、分类、reindex
    ├── chapters.js         #   章节标题正则与固定块降级
    ├── encoding.js         #   BOM → 严格 UTF-8 → GB18030 探测
    ├── paths.js            #   路径闸与目录闸(所有落盘先过它)
    ├── atomic-json.js      #   原子写 + revision CAS
    ├── notes.js            #   笔记:机器锚点、分页(游标)、草稿、旧文件兼容
    ├── tags.js             #   确定性 tag 词表(零模型调用)
    ├── background.js       #   背景认识:分区解析、注入渲染、裁剪、人物卡
    ├── background-update.js#   改块提示词与字段校验
    ├── memory.js           #   缺口计算与补齐循环
    ├── compact.js          #   压缩(四条硬校验)与历代备份
    ├── spoiler.js          #   守则 / 情况 / 读窗 / 讨论的 prompt 装配 + 注入体积度量
    ├── discussions.js      #   讨论时间线
    ├── export.js           #   导出:标记、消歧、只追加、历代快照
    └── subagent-run.js     #   借宿主会话跑补齐调用
scripts/
├── link-into-profile.mjs   # 本地开发:往 profile 里建目录联接(纯加法,--unlink 回滚)
├── reindex-books.mjs       # 让已导入的书吃到新的切分规则(默认预览,--apply 才写)
└── clean-background-note.mjs # 清理 background.md 的注释残留(默认预览,需 --file 指定)
docs/                       # design-v1(追加式修订块)/ manual-testing / publishing

关键设计:

  • 单一坐标:进度是唯一坐标,投喂窗口、缺口、闸门、倒退过滤全都从它派生——好处是能力之间不打架,代价是它滞后就会连锁出错(面板因此同时显示「读到第 N 章 · 记忆到第 M 章」)。
  • 硬闸与启发式分开:路径闸是硬保证(与会话无关),联网闸是启发式(明说挡不住刻意查询),提示词守则常驻。不把启发式包装成保证。
  • 只增不减:笔记只追加、背景只增条目;压缩是唯一会删的一步,且有四条硬校验 + 历代备份。
  • 客户端半边必须自包含:宿主把它当构建产物整份读取,所以 lib/client.js 不能拆多文件。
  • 不改写用户的历史:background.bak.* 一代不删,导出的"压缩前"一代一个文件;合并这类需要判断的事交给外部工具。

开发

npm test                                  # 全部测试(Node 内置 test runner,577 项)
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 路径>   # 清理注释残留(默认预览)
  • 改完即生效:lib/ 就是源码,重启 DSH Desktop 即可(本插件没有构建产物,所以也没有"改 src/ 触发重载"那一层)。
  • 改切分规则不会自动作用于已导入的书(导入是幂等的),所以老书要么删掉重导(丢笔记、丢进度),要么用 reindex-books.mjs 就地重切。
  • CI:GitHub Actions 跑 node --test,矩阵 Node 22.19 / 24 × ubuntu / windows。⚠️ 不要在 CI 里加 --test-isolation=none——那个开关在 Node 22.19 上不存在,会让两档以退出码 9 当场失败(v2.0.4 首发时真踩过,见 docs/design-v1.md v1.40)。
  • 代码结构、测试清单、以及客户端测试替身的盲区都在 CONTRIBUTING.md。

贡献者

贡献者负责
xling001功能设计、方案取舍、真机验证
AI(DSH 内的编码 agent)代码实现、测试、文档

本仓库的代码主要由 AI 编写。 人类作者负责提出要解决什么问题、在几个方案之间做选择、以及在真机上发现"哪里不对"。

与同类插件的区别,以及参考了哪些插件

同类里定位最接近的是 dsh-reader(用 DOM 选择器冒充插槽,在本机 DSH 上中央列会被清空,且没有任何 AI 机制)与 dsh-novel-forge(创作工具台,本项目只读)—— 本项目只往官方插槽注册,页签落在右侧栏、不接管中央列,因此与 dsh-tavern 这类插件共存无冲突。具体借鉴的是几处模块:dsh-reader 的编码探测顺序与章节正则基线(连同它几个真实故障的反面教训)、dsh-tavern 的路径闸做法、官方 dsh-client-ui-sidebar-right / dsh-client-modules 的客户端半边写法与发现契约;dsh-adaptive-context 提供过一条踩坑形状,dsh-novel-solo / dsh-talebook-plugin 只做过定位对比。出处都在源码注释里(grep dsh- 就能找到)。另有一个不同形态的参考:对坐 duizuo-reading-companion-skill(Agent Skill,MIT)—— 我们只借鉴了它守则里"防说"的那一半:元剧透清单、引文只能来自原文、来源声明与"读者优先于二手来源"。它"防读"的那一半(阅读范围 / 阅读单元 / scope_handle 契约 / 前后材料隔离)没有抄:那些是为"电子本就在模型手边"设计的,而我们的投喂层已经结构性地不含后文 —— 判据是「凡是在防"读"的不抄,凡是在防"说"的抄」。

许可

MIT

관련 플러그인