# 真机自检清单(P1–P21) > 这份清单原先长在 `README.md` 的「验证」章节里,占了公开说明约三成篇幅。 > 它是**发版前的真机回归清单**,不是用户文档,所以搬到这里单独放。 > > 自动化测试(`npm run test:no-isolation`)覆盖宿主逻辑、HTTP 契约与纯函数, > **覆盖不到界面与真实模型调用**——下面每一项都需要人点一遍。 --- 1. 打开任意会话,点右侧栏的「**+**」; 2. 列表里应出现「**陪读**」(一本摊开的书的图标); 3. 点开后面板进入**书架**视图。 > **看不到「陪读」时先看这里**:右侧栏那个选择器的条目**完全**由插件注册的 > `guide` 数组构建(见 `docs/design-v1.md` v1.5 §31)。所以看不到它意味着 > 客户端半边没挂上,而不是"被藏起来了"。注意**客户端半边的改动需要重启 > DSH Desktop**,光刷新页面不保证生效。 **P1 端到端自检**: 1. 把一本 TXT 放进书库的 `inbox/`(面板里会直接显示这个路径), 或直接在输入框里粘贴文件的绝对路径; 2. 点「扫描导入目录」→ 点条目右侧的「导入」; 3. 书架出现书名与「N 章」;点书名进入**目录**; 4. 点任意一章进入**正文**,滚动到底再点「下一章」; 5. 返回书架 —— 该书的进度应显示「读至第 N 章」。关掉页签重开, 仍能落回原来的位置。 **P2 防剧透自检**(这是本插件最该被验证的一条): 1. 打开一本书,进入正文,**滚到中段**(让进度明确落在某一章中间); 2. 右上角「陪读」→「把本会话绑定到这本书」; 3. 点「查看 AI 现在能看到什么」; 4. 在返回的文本里搜索**后面章节的独特人名/地名/关键词** —— 一个都不该出现; 5. 往下滚到最后一章再看一次:此时才应该出现结尾附近的内容。 第 4 步是决定性的。如果那里出现了还没读到的内容,那就是防剧透失效,请开 issue。 **P3 笔记自检**: 1. 在正文里选中一段 → 点「记笔记」→ 摘抄已自动填入,tag 框里已有建议; 2. 写一句感想(比如"这段文笔真好")→ 点「**写入笔记**」(回应框空着,按钮就是这个文案); 3. 打开 `$DSH_HOME/dsh-reading-companion/books//notes.md`: 应当有摘抄、感想、tag,**而且搜不到「AI 回应」四个字**; 4. 再记一条,这次把一段文字填进「AI 回应」框(或点「② 抓取选中文字作回应」), 按钮会变成「**写入笔记(含回应)**」→ 这次 md 里才有那一段; 5. 在 md 里手写一行「**这条不能被吃掉**」,回去再记一条笔记 —— 你手写的那行必须原封不动。 > **编号里没有 P4** —— 它原先测的是「笔记分页与渲染隔离」,已经并入下面的 **P7**。 > 保留这个空号是为了不打断历史记录里对 P5 之后各项的引用。 **P5 背景认识自检**: 1. 打开一本书,读到第 3~4 章; 2. 「陪读」页 →「背景认识」应显示 `还没有建立… · 缺口:第 1–N 章`; 3. 点「**补齐前文记忆**」—— 会**等几秒到几十秒**(这是一次真实的模型调用); 4. 回来后应显示 `记忆到第 M 章 · 已知人物 K 位`,缺口缩小或消失; 5. 点「查看 / 校对」看 `background.md` 原文:应有人物关系、人物、世界观、前文脉络; 6. 再往第 6~7 章读一段,回来点「补齐」——**旧条目必须还在**,新条目追加在后面。 这一步能验证三件事:记忆真的建起来了、缺口真的在缩小、**条目只增不减**。 **P6 笔记落点与字体自检**: 1. 打开一本书 →「笔记」页 →「笔记保存位置」应当显示 `<你的工作区>\陪读_<书名>`,并注明「落在会话工作区里」; 2. 去资源管理器打开那个路径 —— `notes.md` / `background.md` / `README.md` 都在,**没有 `content.txt` 那种大文件**; 3. 如果显示的是插件目录:说明这本书是**绑定之前**就记过笔记的老数据。 点「**重新检测位置**」,它应当搬到工作区,并提示 `(已把 notes.md 复制过来,原文件保留)`; 4. 再导入第二本书并绑定 —— 两本书应当各有一个 `陪读_*` 文件夹, **内容不串**; 5. 正文页右上角「**Aa**」:调字号、行距、字体,滚一下再切章 —— 设置应当保持(存在浏览器本地)。 第 3 步是这次改动最容易出问题的地方(老数据迁移),所以专门列出来。 **P7 笔记分页自检**(笔记多了才看得出差别,可选): 1. 在同一本书里连续记 **25 条以上**笔记(摘抄随便填几个字即可); 2. 打开「笔记」页 —— 标签应显示 `已落盘的笔记(25)`,但**只列出 10 条**,底部显示「第 1 / 3 页」; 3. 点「更旧 →」→ 列表**整页替换**成第 2 页(10 条),「← 更新」变为可用;一路点到第 3 页 (5 条)时「更旧 →」应禁用; 4. 点「← 更新」能逐页回到第 1 页(**不是**把各页累加成一个长列表); 5. 回到正文再记一条 → 回列表:应回到第 1 页,新笔记在**最前面**,且**没有重复条目**。 第 2 步的"标签是 25、列表是 10"是刻意的:`total` 是全部条数(你要知道自己记了 多少),分页只是渲染上的节制。 **P8 书友设定 / 讨论历史 / 缓存前缀自检**: 1. 「陪读」页 →「**书友设定(你写给 AI 的)**」写一句能一眼认出来的话 (比如「说话不要用感叹号」)→ 点「保存设定」→ 按钮变成「已保存」, 下面显示 `N / 4000 字` 与 `persona.md` 的完整路径; 2. 点「查看 AI 现在能看到什么」—— 在返回的文本里搜那句设定,应当找到, 而且它出现在 `## 书友设定` 这一段里,**在守则之后、背景认识之前**; 3. 同一页面看 `可缓存前缀 N 字(占 X%)`。翻到下一章再刷新一次预览 —— 这个**数字应当基本不变**(变的是它后面那一段); 4. 回「笔记」页记一条笔记 → 回「陪读」页最下面,「**讨论历史**」里应出现 一条「写了笔记 · 第 N 章」; 5. 在会话里聊完、抓回回应 → 讨论历史里应再多一条「抓回回应」; 再打开预览,`你们之前聊过` 这一段里应出现你那条感想的一句话摘要。 第 3 步是这次改动的核心:**前缀稳定 = 缓存命中 = 省钱**。那个数字如果每翻一章 都大变,说明有人把动态值挪回了稳定区。 **P9 背景压缩自检**(背景认识很长时才做,可选): 1. 「陪读」页看背景认识的 `记忆到第 N 章 · 已知人物 K 位`; 2. 如果预览里出现 `背景认识已超出预算,本次有内容被省略:…`,说明它胖了; 3. 点「**压缩背景认识**」→ 应提示 `背景认识已压缩:A → B 字`,并给出 `background.bak.md` 的路径; 4. 再点「查看 / 校对」——**人物一位都不能少**,条目的 `` `第N章` `` 标记还在; 5. 万一提示 `压缩结果丢了人物(…),已整批丢弃` —— 那是安全校验拦住了它, **你的文件一个字都没动**,重试一次通常就好。 第 4/5 步是压缩这件事能不能被信任的关键:它是"只增不减"唯一的例外, 所以宁可整批丢弃,也不接受一次可能丢了人物的压缩。 **P10 笔记格式自检**(改的是你已经在用的文件,值得看一眼): 1. 在**有章节**的书里选中一段 → 记笔记、填感想、填 tag → 「写入笔记」; 2. 打开 `notes.md`:那一条的**标题行**应当形如 `### 第16章 带子 #人设`—— 章节与 tag 在**同一行**,tag 不再单独占一行; 3. 标题行下面依次是 `> 摘抄` → `**我的感想**:…` → `**AI 回应**:…` (回应框空着时**没有** AI 回应那一段); 4. 回「笔记」页看这一条:**tag 不会显示两遍**(标题行里那份会被精确剥掉); 5. 如果书架里有**没有章节**的书(导入时提示 `strategy: fixed-blocks`), 在那里记一条,标题行应当只剩 tag。 第 4 步是最容易做错的地方:tag 现在同时存在于**属性**和**标题行**里。读回来时 必须按属性里记着的那几个精确剥掉——按"见到 `#` 就当 tag"切,会把 `第3章 C# 入门` 这种标题一起切坏。 **P11 发到会话 + 书架分类 / 绑定 / 跳转自检**: 1. 「笔记」页填好摘抄与感想 → 点「**① 发到会话去聊**」→ 会话输入框里应当以 `**第16章 带子**` 开头,然后是引用块摘抄、再是感想(**不会自动发送**); 2. 回「书架」:书籍按分类分组,**「未分类」永远排在最上面**并带一个条数; 3. 点某本书右侧的分类下拉 →「+ 新建分类…」→ 输入名字 → 「确定」→ 书应移动到新分组,下拉里也多出这个选项; 4. 选回「未分类」→ 书回到最上面那一组。如果那个分类里再没有别的书, **它应当从下拉里整体消失**(分类存在当且仅当有书属于它); 5. 每本书右侧显示「未绑定」或「**已绑定 · 跳过去**」。点后者 —— DSH 应当切到 那本书绑定的会话。(若是静态的「已绑定会话」而不是按钮,说明这个宿主没有 `sessions` 服务,跳转不可用;其余功能一律照常。) 第 5 步刻意做成**按钮**而不是"点书籍就跳":点书籍本身是"打开这本书", 两个动作抢同一个点击是最容易让人误操作的设计。第 4 步则可以验证那条设计: 分类是独立文件 `categories.json`,而且**不维护分类清单**——所以删掉最后一本 属于它的书之后,它不该还留在下拉里。 **P12 联网档位自检**(这一档原先只能改配置文件再重启): 1. 打开一本书 → 「陪读」→「AI 视角预览」→ 找到「防剧透闸」那一段; 2. 三个按钮「**完全** / **本书** / **关闭**」,当前生效的那个是**高亮**的; 3. 点「关闭」→ 下方那行应变成「当前生效:关闭(不拦)。改完**立即生效,不用重启**。」 ——**不要重启**,留在这一页直接进行第 4 步; 4. 点「重新生成」看预览:守则里那句应当从「**不要联网查这本书**」变成 「**联网只用来查设定,不用来查剧情**」; 5. 点「完全」→ 预览里那句应立刻变回去。 第 3、4 步是**同一次运行内**完成的,这就是"不用重启"的意思:档位是在每次装配 prompt 时现读的。(`settings.json` 本身只在启动时读一次——那是刻意的,热路径上 每次工具调用都同步读盘不划算。所以**手动**编辑那个文件仍需重启;界面上的开关会 同步更新缓存,不需要。) 第 5 步顺便能看到一条刻意的设计:「完全」与「本书」给模型的**措辞是一样的** (都让它别联网查这本书),区别只在**工具闸的严格程度**——前者一律拒绝,后者 只拦看起来在查本书的查询。 **P13 文风 + 抽样打底自检**: 1. 找一本**你读到 30 章以上、但还没建过背景认识**的书(或用「清空重建」造一个); 2. 点「补齐前文记忆」→ 等一次调用; 3. 看 `background.md`:应当出现 `## 文风` 一节,内容只**描述**叙述特征 (视角、句式、用词、节奏),**不评价好坏**; 4. 同一次结果里,前 5 章的条目应当明显比后面几章详细——那是 3 倍加权; 5. 覆盖区间应当是 `covered=1..30` 左右,而**不是**一路铺到你的当前章。 再点一次「补齐前文记忆」,这一次应当把剩下的**一趟补完**。 第 3 步的"只描述不评价"是刻意的:整理记忆的子代理原先被明令**禁止**写文风 (`MEMORY_PERSONA` 第 2 条),现在放开了,就必须换成"可以写特征、不要评好坏" 这个更精确的说法。第 5 步是"打底"模式——它只在**第一次**补齐时生效。 **P14 讨论历史 + 阅读排版自检**(本版新增): 1. 打开一本聊过 6 次以上的书 → 「陪读」页的「讨论历史」应当**只列 5 条**, 下面有一行「共 N 条,这里只显示最近 5 条。」; 2. 在正文页「**Aa**」里把字号从 16px 一路拉到 24px:**章节标题要跟着一起变大**, 并且**始终比正文大**。从前标题被写死成 15px,字号一调大它就"塌"进正文里, 章节和段落的层级会整个消失; 3. 同一操作下,**每行字数不应变化**,只是整栏变宽——行宽是按 `em` 算的。 「一行多少字」才是阅读舒适度的自变量,像素宽度不是。 第 1 步那行字是刻意留的:**「被截断」必须可见**。不说的话,你会把这 5 条 当成全部历史。落盘上限是 200 条(`MAX_DISCUSSIONS`),面板只取最近 5 条—— 列表的用途是回答"我们上次聊到哪了",它是导航时间线,不是账本。 **P15 本轮修掉的真问题自检**(都需要在真机上点一遍才能确认): 1. **「读到哪记住哪」**——这是最要紧的一条。打开一本书,滚到中间,停手等 1–2 秒 (进度在停手 1.2 秒后回写),然后**退回书架、再打开同一本书** → 正文应当 **落回你刚才的位置**,而不是停在章首。切到下一章再切回来也一样。 > 这条此前**是坏的**:恢复位置用的是一次性布尔旗标,而组件挂载那一刻正文 > 还没到(`paragraphs` 是空数组),旗标就被提前消耗了;等正文真的到达, > 旗标已是 `true` → 直接返回,于是**再也没有人滚动过视口**。表现是"每次都停在 > 章首",可进度在服务端存得好好的、百分比也显示正确——**从界面上完全看不出 > 它是坏的**。顺带修掉的是:自动进度回写会把视口往上拉 12px。 2. **记笔记时摘抄要出现在编辑框里**。在正文里选一段字 → 点浮出的「记笔记」→ 笔记页的**摘抄框里应当已经有你选的那段原文**。 > 这条此前**也是坏的**,而且**两份外部审查报告都没抓到**:`NotesView` 把 > `activeDraft` 只读进 `useState` 的**初值**,而它是在草稿还是 `null` 的那一刻 > 挂载的(草稿要等一个网络来回)。测试替身不做状态更新,所以这条路径在单测里 > 渲染不出来——它是靠读时序发现的。 3. **切书不再串数据**。快速连点两本书(先点章节多的、立刻再点另一本)→ 目录、 章数和标题必须属于**同一本**书。 4. **目录筛选**(50 章以上才显示)。打开《一世之尊》(**1404 章**),目录上方应当 出现一个输入框,占位文字是「在这 1404 章里找(章号 / 标题 / 卷名)」: - 打 `812` → 应当**只筛出章号 812** 那一条; - 打 `1` → 应当能搜到**第 1 章**(章号是 **1 起**,和目录里显示的一致); - 打标题里的字(如「归来」)或卷名(如「卷七」)也要能筛出来; - 「清除」按钮恢复完整目录;筛不到时应当明确说"没有匹配的章节",而不是空白。 > 如果打 `1` 搜不到第一段,说明章号被按 0 起算了。这类失效**不会报错**, > 只会表现为"这本书好像没有这一章"。 **P16 「发到会话」要发对地方自检**(本版新增): 在**另一个会话**里,打开一本绑在**别的**会话的书(书架那一行标着「已绑定 · 跳过去」 的就是),写一条笔记,点「① 发到会话去聊」: 1. 等前文记忆检查完,界面应当**自动切到那本书绑定的会话**; 2. 切过去之后,那边的**输入框里应当已经有你的摘抄与感想**(以 `**第 N 章 …**` 开头); 3. **仍然不会自动发送** —— 这一条和以前一样,要你自己按回车; 4. 如果那边输入框里本来就打了字,你的摘抄应当**接在后面**,而不是把它冲掉。 > 这一条此前**是坏的**:文字进的是**当前**会话的输入框。根因不是"忘了跳转", > 而是投递目标**本来就只能是当前会话** —— `inputActions` 由宿主**按会话**交给 > 面板,插件拿不到别的会话那一份(宿主自己的 `inputHub.shell(id)` 不在插件可达的 > 服务面上)。所以只能"先跳过去,再把文字交到那边"。 > > **一个已知的静默失败(v0.14.0 已大幅收窄)**:交接要有人来接。如果目标会话那边 > 始终没挂上面板,交接会在 2 分钟后过期丢弃 —— **不落字、也不报错**。v0.14.0 起 > 跳过去时会尽力自动打开页签(见 P17),所以这条路径已经很少走到;真遇到时请手动 > 把摘抄复制过去。 **P17 「跳过去之后还在读那本书」自检**(本版新增): 接着 P16 的场景(在另一个会话里点「① 发到会话去聊」): 1. 跳过去之后,右侧栏应当**自己打开「陪读」页签**,直接落在那本书的**正文**上, 而不是要你重新点侧边栏、再点一次那本书; 2. 那个页签里应当停在**你刚才读的那一章**(位置由服务端进度决定,与源会话一致); 3. 那边输入框里应当有 P16 说的那段摘抄。 4. **(v0.14.3 改)跳过去应当在**笔记页**,而且是同一条草稿。** 你是在笔记页点的发送, 所以要接着开笔记页:摘抄框、感想框、回应框里的内容都还在,方便你一边和书友聊、 一边往下补。**不是在正文,也不是在目录。** 5. **(v0.14.3 改)在那边点笔记页左上角的「←」,才回到来源那一层。** 从**正文**里选 一段进来的回**正文**(停在你原来那一段);从**目录**点「笔记」进来的回**目录**。 > ⚠️ **第 4、5 条前后错了两次,值得记下来**: > > - **v0.14.0**:落点被 `resolveRestoreView` 刻意映射成**目录**(当时的顾虑是"笔记页缺 > `activeDraft` 就只剩空编辑框,还原过去比回目录更糟")。于是"从正文选一段、记笔记、 > 发到会话"稳定地落到目录页。 > - **v0.14.1**:改成拿"进笔记页那一刻的来源"当落点。**方向修错了** —— 从正文选段的 > 确实回正文了,但读者**正在写的那条笔记从眼前消失**,还得重新点进来一次。 > - **v0.14.3**:把两件事拆开 —— **「发送」保留此刻这一层,「返回」才回到来源**。 > 笔记页从此是**有条件**还原的:草稿在(交接会把它一起交过来)就还原笔记页,草稿不在 > (切页签、刷新、同会话自己重挂)才退回目录 —— 后者若硬还原,读者会看到一个空编辑框, > 以为摘抄丢了。 > > 一句话:**"来源"这种状态一旦被记下来,就要找出它的全部消费者。** 只改一个(比如只让 > 返回按钮用它、交接却继续用旧的映射)会让功能自相矛盾:同一个来源、两套落点。 > > 这个缺陷还有第二层,而且形状和本轮早些时候修掉的"位置恢复"**一模一样**:待落点项 > 在**挂载那一趟**就被消费掉了。挂载时 `catalog` 还是初始的 `{chapters: [], loading: > false}`,看起来"已经加载完、并且章节为空";而 `openBook` 要到**同一次提交的另一个 > effect** 里才把它置成 loading。老代码在这一趟清了待办、又因为章节为空而 `return`, > 等目录真到位时已经没得还原。现在只有真正落点(`apply`)才消费,`stay` 一律原样保留。 **P18 正文顶部的「笔记」按钮自检**(本版新增): 在**正文**页,**不要**选中任何文字,点顶部那颗「**笔记**」: 1. 应当直接进入笔记页:能看到已落盘的笔记列表、草稿列表,也能手写新建一条; **并且草稿栏里不会多出任何条目** —— 你没保存过任何东西(见 P19); 2. 在里面点「←」,应当回到**正文**(你正是从正文进来的)。 > ⚠️ **这一条是 v0.14.3 修的缺陷**:`ReaderView` 内部一直都在调 `onOpenNotes`,但 > `ReaderPanel` **忘了把这个 prop 传下去** —— 于是那颗按钮点了**毫无反应**,读者不先 > 在正文里选中一段就进不了笔记页。 > > 值得一提的修法:渲染层根本不认识"哪个 prop 忘了传",它只是**安静地什么都不做**, > 所以这个洞只能靠用例堵。测试里现在钉的不是那一个名字,而是**整条规律** —— > "`ReaderView` 解构出来的每个 prop,`ReaderPanel` 渲染它时都必须给"(两种传递写法 > 都算数:`name: value` 与简写 `name`)。将来再加 prop 忘了传,会直接报红。 **P19 「草稿栏只装你亲手存过的东西」自检**(本版新增): 1. 在正文里**选中一段**、点「记笔记」进笔记页,然后**任何保存按钮都不要点**, 直接看「未落盘的草稿(N)」那一行:**不应当有新条目**(N 不变,或那一行根本不出现)。 编辑框里应当已经有你选的摘抄,底下还有一行 「**尚未保存 · 点「保存草稿」才会进草稿栏**」。 2. 点「**保存草稿**」:草稿栏里**这时才**出现一条,那行「尚未保存」随之消失。 3. 回正文再选一段、再进一次笔记页:草稿栏里**仍然只有第 2 步那一条** —— "只是进来看一眼"不会再留下一份新草稿。 4. 写完感想**先别保存**,切到别的页签再切回来:摘抄与感想**都还在**(它们靠插件自己的 会话记忆活着,不是靠服务端)。 > ⚠️ **为什么这条重要**:从前 `captureNote`(从正文选区进笔记页)会**立刻** POST 一条 > 草稿,于是"我只是点进来看一眼"也会在草稿栏里留下一条读者从没保存过的记录。而草稿栏是 > "我存过哪些"的清单,不该被浏览动作污染。现在它和正文顶部那颗「笔记」按钮**完全一致**。 > > 代价说清楚:**未保存的内容只在内存里**。它靠"编辑内容上报给面板 → 面板写进模块级会话 > 记忆"这条线撑住切页签/切会话(第 4 条),但**整页刷新会丢**(那种情况下读者本来也回了 > 书架,重新选一次即可)。 > ⚠️ **第 1 条是"尽力而为",不是保证。** 让宿主在**别的**会话里打开页签,靠的是 > 调用宿主的 `sidebarRight` 服务;而它的 `openTab` 用的是"当前**已挂载**的侧边栏" > 那份 binding,`sessions.open()` 之后要等一次 React 提交才挂上,所以这里只重试 > 几次(150ms ~ 1.1s)。右侧栏被收起、或宿主改了那个接口的形状时,页签就打不开 —— > 此时**降级为"自己点一下页签"**,而书、章、位置、摘抄都已经就位,点开就能看到。 > > **为什么"保持阅读界面"这件事只能由插件自己记**:右侧栏页签在宿主里是**按会话** > 的(槽 `scope: session`),切会话会把这个面板**卸载重建**。所以任何放在组件状态 > 里的东西都会被跳转带走 —— 这正是 v0.13.0 失败的确切原因(交接棒存在 `useState` > 里,跳转把它一起卸载了)。现在"读到哪本书、哪一页"和"要带过去的摘抄"都放在 > **插件自己的模块级内存**里(本插件在整个页面只加载一次,因此模块级变量跨会话 > 存活),重开时按会话 id 取回。这一层不触碰任何平台契约,平台完全看不见它。 **P20 跳读闸 + 倒退过滤自检**(本版新增,**这是唯一不可逆损害的防线**): 1. 打开一本**章数很多**的书(≥ 100 章)。**不要**从第 1 章开始,直接从目录点开**很靠后**的 一章(例如第 150 章); 2. 进「陪读」页点「**补齐前文记忆**」:应当**不开始补齐**,而是出现一个黄色框,写明 「这次要补的是第 1–149 章,共 149 章,超过跳读闸(50 章)」,并给三个按钮: **这些我都读过 / 只记最近 50 章 / 先不补**。 ⚠️ 此时 `background.md` **一个字都不该变**,**也不该出现「补齐中…」**(意味着一次模型 调用都没发)。 3. 点「**只记最近 50 章**」:等一次调用后,记忆应当从**第 100 章**开始,**不是从第 1 章**。 打开 `background.md` 确认头部是 `covered=100..149`,且里面**没有**第 99 章以前的条目。 4. 点「清空重建」,重做第 2 步并选「**这些我都读过**」:这次照旧从第 1 章打底 (补到第 30 章,并提示「缺口较大,只补了前一段」)。 5. **倒退过滤**(这一步是重点):接着上一步,从目录点回**第 20 章**,进「陪读」页看 「AI 视角预览」: - 预览里应当有一行「⚠️ 这份背景认识覆盖到第 N 章,而你正在读第 20 章。第 21 章及以后的 条目**已被过滤**」; - 并且**看不到**第 21 章以后的任何条目; - 而 `background.md` 文件本身**没有被改写** —— 过滤只发生在"投喂给模型"这一步。 6. 连续往前读第 21、22 章:「AI 视角预览」里的背景认识**不再变化**(不倒退就不过滤), 这样缓存前缀才不会被每翻一章就作废。 > ⚠️ **为什么这一条要先做**:读者从目录点开第 1000 章时,自动补齐会把**第 900 章的条目** > 写进 `background.md`,而这份文件之后**原样注入**。等他回到第 50 章老实读,那些条目就是 > 静默剧透,而且**没有干净的补救** —— 唯一的办法是「清空重建」,那会连真读过的记忆一起 > 清掉。所以闸门不是"禁止补齐",是**先问一句**;两个答案都诚实,只是代价不同。 > > 第 5、6 条测的是另一半:判据必须是"**倒退**"而不是"有任何超前条目"。过滤会让背景这一段 > 随进度变化,而它是缓存里最值钱的稳定前缀——**常开会让每次翻章都作废前缀**。 > > 一个容易写错的地方(已有专测):`covered.last` 是 **1 起**章号,`chapterIndex` 是 **0 起** > 索引,两者正好差 1。"允许记到当前章为止"要用 `chapterIndex + 1`;写成 `chapterIndex` > 会把**你正在读的那一章**的条目当成剧透丢掉。 **P22 上一章尾部 + 非小说兜底自检**(本版新增): 1. **上一章只给尾部**:把一本书读到第 10 章以上 → 「陪读」→「查看 AI 现在能看到什么」; - 小节标题应当是「**上一章结尾**」,紧跟一句"前面的内容未提供"; - 面板摘要应当写成「上一章**结尾** N 字」(只报字数的话你会以为那是整章); - 把 `window.previousChapterMode` 改成 `'full'` 重启后,标题应变成「上一章全文」、 那句提示应当消失。**这一步是"改口"是否真的生效的证据。** 2. **非小说兜底**:找一本**不是小说**的文本(史书 / 教材 / 技术书)导入,读几章后点 「补齐前文记忆」,然后打开 `books//background.md`: - 应当出现 `## 通用概念`,概念按 `### 概念名` 分组; - ⚠️ 反过来也要看一眼:**前面的五节该空就得空**。能归进「世界观」「人物」的东西 被塞进兜底,说明提示词里那条"能归就归进去"没被遵守(那是这一节唯一的使用规则); - 再让陪读 AI 聊一个你刚读到的概念,它应当用得上兜底里的内容。 3. **抽样形状**(可选,看数字就够):**默认是"按预算均分"**(`sample.lengthRatio = 0`、 `minPerChapter = 100`)——短章(笔记体、段子、诗歌)能整章装下。 想换成"按章长比例"就把 `sample.lengthRatio` 设成 `0.2` 并把 `minPerChapter` 降到 60, 那时几千字的长章拿到的样本会比几百字的短章明显多。**代价要知道**:比例模式下, 均分模式里能整章装下的短章只拿到下限那一小段。v1.24 曾把比例设为默认,v1.25 按 你的要求退回了均分。 4. **背景认识的分层降级**:这一步只在"背景认识已经胖到超预算、但还没压缩"时才看得见, 平时跳过即可。做法:把 `window.backgroundBudgetChars` 临时调到很小(比如 800), 打开「查看 AI 现在能看到什么」: - 超预算的分区里,一部分主体应当**只剩 `### 主体` + 一条记载**,后面跟一句 「本节有 N 个主体只列出最近一条记载」; - **留下来的那条应当是章号最晚的那条**,不是最早的; - 关掉这一级(`window.backgroundCoarseDegrade: false`)重启后,那句说明应当消失, 那些主体应当变回**整块不见**(而不是变全)。**这一步是"它只补位、不替换"的证据。** - 最后把 `backgroundBudgetChars` 改回去。 **P21 prompt 缓存命中率实测**(**发布后第一次真实阅读时做**,不阻塞发布): 这是"还能不能更省"的**唯一闸门**。背景认识(默认 9000 字)是每轮重发的大块;它**值不值得 缓存**,决定了能不能靠"把背景认识做大"来提升记忆质量(设计稿 §188–190 的未决项)。 做法 —— **必须用 provider 实报数字**: 1. 正常读**同一章**,在会话里连发 3–4 轮消息(**不要翻章**); 2. 打开 `dsh-token-meter`,记下第 2、3、4 轮的 `cacheReadTokens` 与 `cacheWriteTokens`; 3. 同时看「陪读」页的 AI 视角预览(对应 `GET /books/:bookId/context` 的 `summary.cacheSplit`),记下 `stable` / `dynamic` 两个字节数。 判读: - 第 2 轮起 `cacheReadTokens` 应当**接近 `stable` 那一块的 token 量**(不是 0,也不是全部); - 若第 2 轮 `cacheReadTokens` ≈ 0 → 稳定前缀**没有**被缓存。此时**先别去动背景认识的内部 排序**(那是白改),先查是不是有别的插件在同一段落之前注入了每轮都在变的内容; - 若明显低于 `stable` 的占比 → 把这几个数字记下来,作为"要不要把讨论时间线挪到已读内容 之后"的判据。 ⚠️ **不要用宿主的「每 token 四字符」估算值做这个判断**:官方文档自己指出那条启发式 **对中文系统性低估 2.5–4 倍**,用它算出来的"命中率"只是噪声。 (顺带记录一个事实:插件自身的预算**全程用字符**、不做 token 换算,所以这条只伤"对比", 不伤预算逻辑。) > **为什么不能用 `curl` 验证宿主路由?** > DSH Desktop 的 web 层(`dsh-host-frontend-static` + `desktop-browser-access`) > 只放行携带 Electron 渲染器令牌(`x-dsh-desktop-renderer`)的请求, > 其余一律 403,连 `/` 也一样。所以 `curl` 拿到 403 **不代表插件没挂上**。 > 要看挂载情况,请查宿主日志: > > ```powershell > Select-String -Path "$env:APPDATA\DSH Desktop\logs\host\dsh-*.log" -Pattern 'dsh-reading-companion' > # [I] [dsh-reading-companion] [reading] host half mounted — api=/dsh-reading-companion/api storage=… spoilerGate=true > ``` > > 反过来,如果插件**没**挂上,`curl` 会拿到 403 而不是 404—— > 因为未匹配的请求会落到 SPA 静态兜底,而它的路径穿越防护对任何非 `dist` > 内路径都回 403(`dsh-host-frontend-static/lib/index.js:52`)。