dsh-novel-writer 6.5.5 → 6.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -10,13 +10,13 @@ English | [中文](./README.md)
10
10
  [![GitHub stars](https://img.shields.io/github/stars/siweina/dsh-novel-writer.svg?style=flat-square&color=orange)](https://github.com/siweina/dsh-novel-writer/stargazers)
11
11
  [![siweina/dsh-novel-writer MCP server](https://glama.ai/mcp/servers/siweina/dsh-novel-writer/badges/score.svg)](https://glama.ai/mcp/servers/siweina/dsh-novel-writer)
12
12
 
13
- **19 tools covering the whole loop: gather inputs before you write → quantify & self-check → work a prioritised fix list → audit the cross-chapter structure.**
13
+ **20 tools covering the whole loop: gather inputs before you write → quantify & self-check → work a prioritised fix list → audit the cross-chapter structure.**
14
14
  One call before writing gathers **16 inputs** (missing one is how style drift starts); after you write, you don't just get numbers — you get a **priority-ordered to-do list** (line-anchored original text, current vs. target values, reference passages, **direction only, never ghost-written prose**); across chapters you can finally see "the thread snapped, a character vanished, a hook was forgotten".
15
15
  Sentence, emotion and semantic analysis all run **on your machine**: a 24MB Chinese model ships with the package,
16
16
  **zero API cost, your manuscript never leaves the device**. Built for DeepSeek Harness (DSH); the same engine is also
17
17
  exposed as a **stdio MCP server** for Claude Desktop / Cursor.
18
18
 
19
- [Website](https://siweina.github.io/dsh-novel-writer/) · [Technical docs (parameters & examples for all 19 tools)](https://siweina.github.io/dsh-novel-writer/tools/) · [Install](#install) · [The writing loop](#the-writing-loop-end-to-end) · [60-second start](#60-second-start) · [See the output](#see-the-output) · [The 19 tools](#provided-tools-19) · [MCP server](#mcp-server-usable-outside-dsh)
19
+ [Website](https://siweina.github.io/dsh-novel-writer/) · [Technical docs (parameters & examples for all 20 tools)](https://siweina.github.io/dsh-novel-writer/tools/) · [Install](#install) · [The writing loop](#the-writing-loop-end-to-end) · [60-second start](#60-second-start) · [See the output](#see-the-output) · [The 20 tools](#provided-tools-20) · [MCP server](#mcp-server-usable-outside-dsh)
20
20
 
21
21
  ---
22
22
 
@@ -48,6 +48,7 @@ Full version history: see [CHANGELOG.md](./CHANGELOG.md).
48
48
  | "I planted a plot thread and forgot to pay it off" | **Plot registry**: add / list / scan / done, automatically recording which chapters mention each thread |
49
49
  | "Character settings contradict each other" | **Five settings tables + continuity audit** (bridge / OOC / outline drift) |
50
50
  | "I can't read the report" | Everything is **tabulated numbers + quoted anchors from your own text** — screenshot-ready |
51
+ | "An error gives me one line, and never says why it keeps happening" | **Diagnostic report** (`novel_diagnose`): reads the local event log and runs **five health checks** (library root / library contents / data-directory corruption / semantic engine / switch snapshot), aggregating repeated **tool × book** trouble spots, top failure codes and actionable suggestions; the report goes through the **share layer** (free text stripped wholesale, every error classified into a fixed code), so it is **safe to paste into an issue** — use it (or the panel's "copy diagnostic bundle") rather than attaching the local log file. The local log is **metadata-first** (tool / book / chapter / status / duration / counts / paths / error code), and free-text fields (error messages) are clamped to **at most one sentence (≤160 characters)**: that local file may therefore echo a fragment of what you passed in, but it **never leaves the machine** |
51
52
 
52
53
  ## Install
53
54
 
@@ -155,7 +156,7 @@ fixAnchors:3 条原著锚段(对话 / 心理 / 描写各一条,供逐句
155
156
  | Cost | **0 (local inference)** | Billed per token | Self-hosted |
156
157
  | Purpose-built for Chinese fiction | **Yes** | Generic | No |
157
158
  | Style baseline (μ±σ) | **Yes** | Rare | No |
158
- | DSH integration | **19 tools + official seats & website-style UI** | None | None |
159
+ | DSH integration | **20 tools + official seats & website-style UI** | None | None |
159
160
  | Usable without DSH | **Yes (MCP)** | Yes | You wrap it yourself |
160
161
 
161
162
  > **A note for non-Chinese users**: this plugin is designed for analysing and writing Chinese fiction — the sentence-pattern,
@@ -179,11 +180,11 @@ fixAnchors:3 条原著锚段(对话 / 心理 / 描写各一条,供逐句
179
180
  10. **Style Baseline**: Six writing metrics (syntactic complexity / modifier density / abstraction / action density / hedging / gap index) + per-chapter μ±σ baseline band; `novel_style_report` outputs the band, `novel_style_check` compares new chapters (in-band ✓ / out-of-band ⚠); per-metric ±% tolerance configurable in the panel's **Six-metric baseline** view (**recommended = 1.5× σ of the book's chapter variance**, rounded, clamped to ±10%~100%; leave blank to use recommended) — free theme, writing style kept inside the band.<br>**Baseline scope (v6.5.5, configured per book)**: the baseline does not have to be the whole book — in the panel's **Style baseline** view choose **whole book** (default, = the old behaviour) or **selected chapters**, then tick the chapters you treat as the yardstick; the baseline then uses **only** those (it means "only these", not "prefer these and top up when short"). This exists for "the AI keeps writing and ends up sounding like the AI": from chapter 2 onward the model is really reading its own previous chapter for reference, and **a baseline sampled from the whole book drifts along with it** (the longer the book, the higher the AI share). Nail the yardstick to the chapters you wrote yourself and the plugin can answer "**relative to those chapters**, how far has this chapter moved" — it never judges whether you have drifted, which is not decidable in the first place. The chapter being checked is **always excluded** (otherwise it would be comparing itself with itself); when fewer than 3 chapters are ticked the recommended tolerance widens to **at least ±25%**, so even a single chapter can serve as the baseline; chapters that cannot be used (renamed / deleted, or **too short to measure**) are skipped and reported honestly, and if not one of them can be used it falls back to **whole book** and states the reason in the returned baseline description. The scope is stored per book (`<library root>/.novel-writer/baseline/<book>.json`), so books do not affect each other.
180
181
  11. **Writing sentinels**: `novel_continuity_check` extended — ①**bridge check** (`chapter`: time jumps / semantic distance / character continuity / hook handoff, with quoted evidence) ②**OOC check** (`ooc`: per-character emotion baseline deviation) ③**outline drift** (`outline`: direction vs body keyword overlap); **brief mode** for report tools.
181
182
  12. **Original mode & creation files**: fill in creation settings in the panel's **Original mode** view (worldview/characters/forbidden/main conflict/genre/extras, blank = model decides, per-book profile library); novel_outline maintains creation files (bible/characters/outline/hooks/status), enforcing the bible → outline → hook chain with dynamic batches (10→20→30 chapters) to prevent plot jumps and OOC.
182
- 13. **Experience & stats**: main panel **library stats** (per-book chapters/total chars/7-day active chars, 🔥 green), **🎬 demo** (built-in sample, no files, runs the 6-dim baseline), **📊 report history** (analysis/style-reports browsing); actionable error hints; slimmer tool descriptions.
183
+ 13. **Experience & stats**: main panel **library stats** (per-book chapters/total chars/7-day active chars, 🔥 green), **🎬 demo** (built-in sample, no files, runs the 6-dim baseline), **📊 report history** (analysis/style-reports browsing), **🩺 diagnostics** (diagnostic report + statistics window + **one-click copy of the diagnostic bundle** for pasting into an issue); actionable error hints; slimmer tool descriptions.
183
184
 
184
185
  ---
185
186
 
186
- ## Provided Tools (19)
187
+ ## Provided Tools (20)
187
188
 
188
189
  > Parameters, return shapes and worked examples for every tool live in the [online tool manual](https://siweina.github.io/dsh-novel-writer/tools/).
189
190
 
@@ -208,12 +209,13 @@ fixAnchors:3 条原著锚段(对话 / 心理 / 描写各一条,供逐句
208
209
  | `novel_chapter_brief` | **Chapter brief** — one call before writing gathers everything (previous-chapter hand-off / chapter direction / characters / open threads / wording rules / style baseline / anchors & skeletons / avoid-list / checklist) |
209
210
  | `novel_fix_plan` | **Fix plan** — turns style diagnostics into a priority-ordered to-do list (line anchors + reference passages), re-checkable after edits; **direction only, never generates prose** |
210
211
  | `novel_lexicon` | **Required-lexicon table** — per-book + global two-layer registry of proper nouns and author-favoured wording, batch import, hard-checklist injection into the chapter brief, and a wording audit for finished drafts |
212
+ | `novel_diagnose` | **Diagnostic report** (v6.6.0) — reads the local event log and runs five health checks (library root / library contents / data-directory corruption / semantic engine / switch snapshot), aggregating recent calls / degradations / failures, repeated **tool × book** trouble spots, top failure codes and actionable suggestions; three actions: `report` / `events` / `clear`. **The report carries metadata only** (tool / book / chapter / status / duration / counts) — free text is stripped wholesale at the share layer and every error is classified into a fixed code, so it is **safe to paste into an issue** (use this or the panel's "copy diagnostic bundle", not the local log file) |
211
213
 
212
214
  ---
213
215
 
214
216
  ## MCP server (usable outside DSH)
215
217
 
216
- The package ships a **stdio MCP server** (`mcp/server.mjs`) that exposes all 19 tools to any MCP client,
218
+ The package ships a **stdio MCP server** (`mcp/server.mjs`) that exposes all 20 tools to any MCP client,
217
219
  e.g. Claude Desktop or Cursor. **It runs as a local process on your own machine — no server, no network, no daemon.**
218
220
 
219
221
  ```bash
@@ -257,6 +259,8 @@ start the server with `--allow-external-src`. Details: [mcp/README.md](./mcp/REA
257
259
 
258
260
  Under `<library-root>/.novel-writer/`: `plots` / `settings` / `summaries` / `analysis` / `audits` (continuity audits + fix-plan lists) / `embedding` / `style-reports`.
259
261
 
262
+ **The event log is deliberately not here**: it lives next to the state file — `~/.dsh/dsh-novel-writer/logs/events.jsonl` (overridable via `DSH_NOVEL_WRITER_STATE`), rotating at 512 KB and keeping the 3 most recent files. Reason: the most common failure is precisely "the library root is wrong", and at that moment `.novel-writer/` does not exist or is not writable — exactly when the evidence matters most. The log is **metadata-first** (time / tool / book / chapter / status / duration / counts / paths / error code), **but it cannot claim "no prose at all"**: free-text fields (error messages) are clamped to **at most one sentence (≤160 characters)**, so a fragment echoed back by an error message can land in the log **on this machine** (measured: when `novel_lexicon` cannot find a term it echoes the term you passed into `detail`). The log file **never leaves the machine**; to share evidence (issue / AI) use the `novel_diagnose` report or the panel's "copy diagnostic bundle" — that path goes through the **share layer**, which strips free text wholesale.
263
+
260
264
  ---
261
265
 
262
266
  ## Dependencies, permissions and failure bounds
@@ -280,8 +284,12 @@ Under `<library-root>/.novel-writer/`: `plots` / `settings` / `summaries` / `ana
280
284
  - Built-in skill: registers its own `novel-writing` skill through `ctx.skills`; it only reads the packaged
281
285
  `skills/novel-writing/SKILL.md`, **writes to no skill directory** and needs no host configuration; hosts without a `skills`
282
286
  service are skipped silently.
283
- - Local HTTP: registers 5 routes inside the DSH Web GUI (state / reveal / reports / demo / update-check), loopback-only;
284
- `allowLanState` defaults to off, so LAN access is denied by default.
287
+ - Local HTTP: registers 8 routes inside the DSH Web GUI (state / reveal / reports / demo / update-check / lexicon / baseline / **diagnose**), loopback-only;
288
+ `allowLanState` defaults to off, so LAN access is denied by default. Logging and diagnosis stay on the machine: the event log lives at
289
+ `~/.dsh/dsh-novel-writer/logs/events.jsonl` (512 KB rotation, 3 kept) is **metadata-first** (time / tool / book / chapter / status / duration / counts / paths / error code) — **free-text fields (error messages) are clamped to at most one sentence (≤160 characters)**, so a fragment echoed back by an error message can appear in that file **on this machine**; the file itself **never leaves the machine**. `novel_diagnose`
290
+ and the panel's "copy diagnostic bundle" go through the **share layer**, which strips free text before anything is emitted (**nothing that leaves the machine carries free text**);
291
+ **the share layer is the default** (`readLogEvents` returns the free-text local layer only when `forShare:false` is passed
292
+ explicitly — a privacy boundary defaults to giving less, never more).
285
293
  - MCP server (`mcp/server.mjs`): the `root` argument of every tool must fall inside the library root given to `--root`
286
294
  (out-of-root values are rejected and fall back); `novel_import`'s `src` is likewise root-limited unless you explicitly
287
295
  opt out with `--allow-external-src`.
@@ -291,7 +299,12 @@ Under `<library-root>/.novel-writer/`: `plots` / `settings` / `summaries` / `ana
291
299
  - Lifecycle scripts: none (no preinstall / postinstall / prepare).
292
300
 
293
301
  **Failure bounds**: semantic engine failure falls back to pure rule mode; cache/disk failures never block
294
- tool results; a plugin load failure cannot affect the DSH host process.
302
+ tool results; **"can't read it" is never reported as "there's nothing there"** — only a genuinely missing file
303
+ (`ENOENT`) is described as "not initialised / not registered / not saved / no creation files"; when the file
304
+ exists but cannot be read (corrupt / no permission / undecodable encoding) the plugin states that fact with the
305
+ path and reason, says an `init` is *not* needed, and records an event you can inspect with `novel_diagnose`;
306
+ - **Known leftover (deferred to the next release, recorded as-is)**: if a chapter file is replaced by a **directory with the same name**, `scanChapters`'s `if (!entry.isFile()) continue` skips it **silently** — the chapter disappears from the chapter table (`novel_chapters` loses one entry), `novel_diagnose` reports **zero** unreadable files, and `novel_chapter_brief`'s `degraded` contains **no** "could not read chapter" entry. On that one path, **"can't read it" is still reported as "there's nothing there"**; when a chapter seems to have vanished, first check whether a **directory named like the chapter** exists in the book folder;
307
+ a plugin load failure cannot affect the DSH host process.
295
308
 
296
309
  **Compatibility**: Node.js >= 22.3 (`engines.node`); DSH >= 0.1.1-rc.2 (`dsh.engines.dsh`).
297
310
 
package/README.md CHANGED
@@ -10,12 +10,12 @@
10
10
  [![GitHub stars](https://img.shields.io/github/stars/siweina/dsh-novel-writer.svg?style=flat-square&color=orange)](https://github.com/siweina/dsh-novel-writer/stargazers)
11
11
  [![siweina/dsh-novel-writer MCP server](https://glama.ai/mcp/servers/siweina/dsh-novel-writer/badges/score.svg)](https://glama.ai/mcp/servers/siweina/dsh-novel-writer)
12
12
 
13
- **19 个工具,覆盖「动笔前取材料 → 写完量化自检 → 改稿按优先级复测 → 跨章结构体检」整条流程。**
13
+ **20 个工具,覆盖「动笔前取材料 → 写完量化自检 → 改稿按优先级复测 → 跨章结构体检」整条流程。**
14
14
  动笔前**一次调用取齐 16 项材料**(漏读一样就漂移);改稿不再只丢给你一堆数字,而是**按严重度排好的待办**(带原句行号、当前值与目标值、原著锚段,**只给方向不代写**);跨章层面能查「线断了、人丢了、伏笔忘了」。
15
15
  句式 / 情感 / 语义全部**在本地算**:24MB 中文模型随包分发,**零 API 花费、正文不出本机**。
16
16
  为 DeepSeek Harness(DSH)打造;同一套能力也可作为 **MCP 服务器**给 Claude Desktop / Cursor 使用。
17
17
 
18
- [官网](https://siweina.github.io/dsh-novel-writer/) · [技术文档(19 个工具的参数与示例)](https://siweina.github.io/dsh-novel-writer/tools/) · [安装](#安装) · [写作流程](#写作流程一条能走完的闭环) · [60 秒上手](#60-秒上手) · [看看输出](#看看输出长什么样) · [19 个工具](#提供的工具19-个) · [MCP 服务器](#mcp-服务器非-dsh-用户也能用)
18
+ [官网](https://siweina.github.io/dsh-novel-writer/) · [技术文档(20 个工具的参数与示例)](https://siweina.github.io/dsh-novel-writer/tools/) · [安装](#安装) · [写作流程](#写作流程一条能走完的闭环) · [60 秒上手](#60-秒上手) · [看看输出](#看看输出长什么样) · [20 个工具](#提供的工具20-个) · [MCP 服务器](#mcp-服务器非-dsh-用户也能用)
19
19
 
20
20
  ---
21
21
 
@@ -47,6 +47,7 @@
47
47
  | "伏笔埋了忘了收" | **伏笔登记表**:add / list / scan / done,自动记录每条伏笔在哪些章被提到 |
48
48
  | "人物设定前后打架" | **设定五张表 + 连贯性审计**(衔接 / OOC / 大纲走偏三件套) |
49
49
  | "报告看不懂" | 全是**表格化数字 + 原文锚段**,可以直接截图分享 |
50
+ | "出错了只给一句话,说不清为什么**反复**出错" | **诊断报告**(`novel_diagnose`):读本地事件日志 + **五处体检**(书库根 / 书库内容 / 数据目录损坏 / 语义引擎 / 开关快照),聚合「工具 × 书名」反复出问题的组合、高频失败类型与可执行建议;报告走**分享层**(自由文本整体剔除、错误一律归类成固定 code),**可安全贴进 issue**——报 bug 请用面板「复制诊断包」或本工具的 `report`,**不要**直接附本机日志文件。本地日志**以元数据为主**(工具名 / 书名 / 章号 / 状态 / 耗时 / 计数 / 路径 / 错误 code),**但自由文本字段(错误消息等)经收敛后至多保留一句(≤160 字)**——所以**本机**日志里可能出现被错误消息回显的输入片段;**日志文件不出本机**,只有**离开本机**的内容才整体剔除自由文本 |
50
51
 
51
52
  ## 安装
52
53
 
@@ -154,7 +155,7 @@ fixAnchors:3 条原著锚段(对话 / 心理 / 描写各一条,供逐句
154
155
  | 花费 | **0(本地推理)** | 按 token 计费 | 自建 |
155
156
  | 中文小说专用 | **是** | 通用 | 否 |
156
157
  | 风格基线(μ±σ) | **有** | 少见 | 无 |
157
- | 与 DSH 集成 | **19 工具 + 官方席位与官网式界面** | 无 | 无 |
158
+ | 与 DSH 集成 | **20 工具 + 官方席位与官网式界面** | 无 | 无 |
158
159
  | 非 DSH 用户可用 | **可以(MCP)** | 可以 | 需自己封装 |
159
160
 
160
161
  > **致非中文用户**:本插件为中文小说分析写作而设计——句式、情感、意象等核心能力以及内置的语义模型,全部针对中文语料构建与调优。在深耕中文的同时兼顾英文等其他语言,确实超出了我目前的能力范围。若因此给您带来不便,我深感抱歉,恳请谅解。
@@ -175,11 +176,11 @@ fixAnchors:3 条原著锚段(对话 / 心理 / 描写各一条,供逐句
175
176
  10. **风格基线**:文笔六维测量(句法复杂度/修饰密度/抽象度/动作密度/不确定性/留白指数)+ 按章节 μ±σ 基线带;`novel_style_report` 输出基线带,`novel_style_check` 对照新章偏差(带内 ✓ / 出带 ⚠);面板「六维基线」视图可自定义每维 ±% 容差(**推荐值 = 原著章节波动的 1.5 倍 σ**,自动取整、限 ±10%~100%;输入框留空即用推荐)——原创/续写时主题自由、写法保持在基线带内。<br>**基线范围(v6.5.5,按书选择)**:基线不必永远是全书——面板「风格基线」里选**按全书**(默认,等于旧行为)/ **按部分**,再用章节选择器勾出你认作标杆的章,基线就**只用**这几章(是「只用」,不是「不够再补」)。这是给「AI 续写越写越像 AI 自己」准备的:从第 2 章续写起,模型参考的其实是它自己写的上一章,而**从全书取样的基线自己也在漂**(书越长 AI 占比越高)。把标杆钉死在你亲手写的那些章上,插件就能回答「**相对这几章**,新章偏了多少」——它不判断你有没有漂移,那一项本就不可判定。正在检查的那一章**永远排除**(否则等于拿自己跟自己比);勾中的章少于 3 章时推荐容差自动放宽到**至少 ±25%**,所以单章也能当基线;取不到的章(已改名 / 已删除,或**正文过短无法测量**)会被跳过并如实计数,**一章都取不到**时自动回退「按全书」并在返回体里写明原因。范围按书保存(`<书库根>/.novel-writer/baseline/<书名>.json`),换书互不影响。
176
177
  11. **写作哨兵三件套**:`novel_continuity_check` 扩展——①**衔接检查**(chapter 参数:时间硬跳/语义距离/人物延续/钩子承接四路检测,带原文引用)②**OOC 检测**(ooc 参数:角色情绪基线偏离)③**大纲走偏**(outline 参数:方向行 vs 正文关键词重合);报告工具支持 **brief 精简模式**。
177
178
  12. **原创模式与创作资料**:面板「原创模式」填写创作设定(世界观/角色/禁忌/主线/题材/额外要求,留空=模型自定,多书独立设定库);novel_outline 维护创作资料(创作设定/人物/剧情大纲/钩子记录/创作状态卡),原创强制「设定书→大纲→钩子」链,动态批次(10→20→30 章)防剧情跳跃与角色 OOC。
178
- 13. **体验与统计**:主面板**书库统计卡**(每本章数/总字数/近 7 天活跃字数,活跃🔥标绿)、**🎬 体验演示**(内置示例不落盘跑六维基线)、**📊 报告历史**(analysis/style-reports 列表浏览);错误提示带解决步骤;工具说明压缩省 token。
179
+ 13. **体验与统计**:主面板**书库统计卡**(每本章数/总字数/近 7 天活跃字数,活跃🔥标绿)、**🎬 体验演示**(内置示例不落盘跑六维基线)、**📊 报告历史**(analysis/style-reports 列表浏览)、**🩺 诊断**(诊断报告 + 统计窗口 + **一键复制诊断包**,用于贴进 issue);错误提示带解决步骤;工具说明压缩省 token。
179
180
 
180
181
  ---
181
182
 
182
- ## 提供的工具(19 个)
183
+ ## 提供的工具(20 个)
183
184
 
184
185
  > 每个工具的参数、返回结构与示例,见[在线工具手册](https://siweina.github.io/dsh-novel-writer/tools/)。
185
186
 
@@ -204,12 +205,13 @@ fixAnchors:3 条原著锚段(对话 / 心理 / 描写各一条,供逐句
204
205
  | `novel_chapter_brief` | **开写包**——动笔前一次调用取齐材料(上一章承接口/本章方向/相关人物/待回收伏笔/用语规范/风格基线/锚段与骨架/禁用清单/开写清单) |
205
206
  | `novel_fix_plan` | **改稿台**——把风格诊断变成按优先级排好的待办(带行号定位与锚段),改完可复测;**只给方向不生成正文** |
206
207
  | `novel_lexicon` | **必用词表**——专有词/作者惯用词的书级+全局两层登记、批量导入、开写包硬清单注入、成稿用词审计 |
208
+ | `novel_diagnose` | **诊断报告**(v6.6.0)——读本地事件日志 + 五处体检(书库根 / 书库内容 / 数据目录损坏 / 语义引擎 / 开关快照),聚合近期调用/降级/失败、**「工具 × 书名」反复出问题的组合**、高频失败类型与可执行建议;`report` / `events` / `clear` 三态。**报告只含元数据**(工具名/书名/章号/状态/耗时/计数),自由文本在分享层已被整体剔除、错误一律归类成固定 code,**可安全贴进 issue**(报 bug 请用它或面板「复制诊断包」,**不要**直接附本机日志文件) |
207
209
 
208
210
  ---
209
211
 
210
212
  ## MCP 服务器(非 DSH 用户也能用)
211
213
 
212
- 包里自带一个 **stdio MCP 服务器**(`mcp/server.mjs`),把 19 个工具原样暴露给任何 MCP 客户端,
214
+ 包里自带一个 **stdio MCP 服务器**(`mcp/server.mjs`),把 20 个工具原样暴露给任何 MCP 客户端,
213
215
  例如 Claude Desktop、Cursor。**它是跑在你自己电脑上的本地进程,不需要服务器、不需要联网、不需要常驻。**
214
216
 
215
217
  ```bash
@@ -249,6 +251,8 @@ npx -y -p dsh-novel-writer dsh-novel-writer-mcp --root /你的小说库路径
249
251
 
250
252
  `<书库根>/.novel-writer/`:`plots`(伏笔)/ `settings`(设定)/ `summaries`(摘要)/ `analysis`(分析报告)/ `audits`(连贯性审计 + 改稿清单 `fix-plan-*.json`)/ `embedding`(语义索引)/ `style-reports`(风格画像)。
251
253
 
254
+ **事件日志不在这里**:它写在状态文件旁——`~/.dsh/dsh-novel-writer/logs/events.jsonl`(受 `DSH_NOVEL_WRITER_STATE` 覆盖),512 KB 轮转、保留最近 3 份。之所以不放书库根:最常见的故障恰恰是"书库根不对",那时 `.novel-writer/` 根本不存在或不可写,偏偏最需要留下证据。日志**以元数据为主**(时间 / 工具名 / 书名 / 章号 / 状态 / 耗时 / 计数 / 路径 / 错误 code),**但自由文本字段(错误消息等)经收敛后至多保留一句(≤160 字)**——**本机**日志里因此**可能**出现被错误消息回显的输入片段(实测:`novel_lexicon` 找不到词条时会把传入的词条名回显进 `detail`)。**日志文件不出本机**;要贴给别人(issue / AI)请用 `novel_diagnose` 的报告或面板「复制诊断包」——那条路走**分享层**,自由文本整体剔除。
255
+
252
256
  ---
253
257
 
254
258
  ## 依赖、权限与失败边界
@@ -271,8 +275,11 @@ npx -y -p dsh-novel-writer dsh-novel-writer-mcp --root /你的小说库路径
271
275
  也限制在书库根内(见下方 MCP 一节)。
272
276
  - 内置技能:通过 `ctx.skills` 注册自带 `novel-writing` 技能,只读取包内
273
277
  `skills/novel-writing/SKILL.md`,**不写入任何技能目录**、不需要改宿主配置;宿主没有 `skills` 服务时静默跳过。
274
- - 本地 HTTP:在 DSH Web GUI 内注册 5 条路由(state / reveal / reports / demo / update-check),
275
- 仅回环地址可访问;`allowLanState` 默认关闭,局域网访问默认拒绝。
278
+ - 本地 HTTP:在 DSH Web GUI 内注册 8 条路由(state / reveal / reports / demo / update-check / lexicon / baseline / **diagnose**),
279
+ 仅回环地址可访问;`allowLanState` 默认关闭,局域网访问默认拒绝。日志与诊断都在本机:事件日志写在
280
+ `~/.dsh/dsh-novel-writer/logs/events.jsonl`(512 KB 轮转、留 3 份),**以元数据为主**(时间 / 工具名 / 书名 / 章号 / 状态 / 耗时 / 计数 / 路径 / 错误 code),**但自由文本字段(错误消息等)经收敛后至多保留一句(≤160 字)**——所以**本机**日志里可能出现被错误消息回显的输入片段,**该文件不出本机**;
281
+ `novel_diagnose` 与面板「复制诊断包」走**分享层**,自由文本整体剔除后才输出(**任何离开本机的内容都不含自由文本**);**分享层是默认行为**
282
+ (`readLogEvents` 只有显式 `forShare:false` 才返回带自由文本的本地层,其余一律脱敏——隐私边界的默认值偏向"少给")。
276
283
  - MCP 服务器(`mcp/server.mjs`):工具参数里的 `root` 必须落在启动时 `--root` 指定的书库根之内,
277
284
  越界会被拒绝并回退;`novel_import` 的 `src` 默认也限根内,确需导入外部目录时用
278
285
  `--allow-external-src` 显式放开。单行报文上限 4 MiB,超限整行丢弃;stderr 默认只记一行错误摘要,
@@ -288,6 +295,8 @@ npx -y -p dsh-novel-writer dsh-novel-writer-mcp --root /你的小说库路径
288
295
 
289
296
  - 语义引擎不可用(模型缺失或 WASM 初始化失败)时自动回退纯规则模式,其余功能不受影响;
290
297
  - 分析结果落盘失败不阻塞工具返回;缓存损坏按"无缓存"处理并重建;
298
+ - **「读不到」不等于「没有」**:只有文件真的不存在(`ENOENT`)才说「尚未初始化 / 未登记 / 尚未保存 / 无创作资料」;文件存在却读不出来(损坏 / 无权限 / 编码识别不了)时只说事实并给出路径与原因,并说明不必去 `init`——同时记一条事件日志,可用 `novel_diagnose` 查真因;
299
+ - **已知残留(下版处理,如实记下)**:把某章文件换成**同名目录**时,`scanChapters` 里那句 `if (!entry.isFile()) continue` 会**静默跳过**它——该章从章表消失(`novel_chapters` 少一章)、`novel_diagnose` 的「读不出来的文件」计数为 0、`novel_chapter_brief` 的 `degraded` 里也**没有**「读取章节失败」。也就是**这一条路径上「读不到」仍被讲成「没有」**;遇到「章数莫名少了一章」时,先确认该书目录下是不是有一个**叫章节名的目录**;
291
300
  - 插件加载失败不影响 DSH 主进程:工具注册与提示词注入相互独立。
292
301
 
293
302
  **兼容范围**:Node.js >= 22.3(`engines.node`);DSH >= 0.1.1-rc.2(`dsh.engines.dsh`)。
package/cordis.patch.yml CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-novel-writer bundle patch: 19 tools (incl. novel_semantic_search, novel_lexicon) + local embedding engine.
1
+ # dsh-novel-writer bundle patch: 20 tools (incl. novel_semantic_search, novel_lexicon, novel_diagnose) + local embedding engine.
2
2
  # dsh-novel-writer bundle patch: mounts the novel-writing assistant plugin
3
3
  # into the profile's loader tree. The entry's name is the plugin package
4
4
  # itself, resolved from the profile's node_modules.
package/lib/analysis.js CHANGED
@@ -1836,6 +1836,8 @@ export function densityOf(text) {
1836
1836
  // 懒加载 + 内存缓存;七类 → 五情感映射,供 sentimentCounts 扩展计数(词表未覆盖的词也能计分)
1837
1837
  import { createRequire } from "node:module";
1838
1838
  import { gunzipSync } from "node:zlib";
1839
+ // v6.6.0:事件日志(log.js 无依赖,可安全被本模块导入)
1840
+ import { logEvent } from "./log.js";
1839
1841
  const __req = createRequire(import.meta.url);
1840
1842
  let DUTIR = null;
1841
1843
  function loadDutir() {
@@ -1844,8 +1846,11 @@ function loadDutir() {
1844
1846
  // v4.0.1:dutir 词表以 gzip 随包分发(原文件 383KB 超出商店单文件上限),加载时解压
1845
1847
  const p = __req.resolve("./lexicons/dutir_seven.json.gz");
1846
1848
  DUTIR = JSON.parse(gunzipSync(__req("node:fs").readFileSync(p)).toString("utf8"));
1847
- } catch {
1849
+ } catch (e) {
1848
1850
  DUTIR = {};
1851
+ // v6.6.0:内置 27k 词七类情感表加载失败 → 整个 dutir 兜底扫描静默失效
1852
+ //(情感统计退回只有主词表,结果数字变小而没人知道为什么)。只随包分发,故基本是打包/权限问题。
1853
+ logEvent("config", { file: "lib/lexicons/dutir_seven.json.gz", level: "warn", detail: "内置七类情感词表加载失败,dutir 兜底扫描失效:" + (e?.message ?? e) });
1849
1854
  }
1850
1855
  return DUTIR;
1851
1856
  }
package/lib/brief.js CHANGED
@@ -43,6 +43,8 @@ import {
43
43
  selectLexiconForBrief
44
44
  } from "./core.js";
45
45
  import { computeBaselineFromPerChapter, judgeAgainstBaseline } from "./style-metrics.js";
46
+ // v6.6.0:事件日志(log.js 无依赖,不会与 core.js 形成循环导入)
47
+ import { logEvent } from "./log.js";
46
48
 
47
49
  // v5.0.0:两档预算——compact 给"够用",full 给"备齐"(characters: 0 语义为"不截断")。
48
50
  // 注意 skeletons 的上限受 buildStyleAnchorPackage 自身产出(最多 4 条)限制,full 的 8 只是"不额外截断"。
@@ -115,7 +117,14 @@ async function readPlotsReadOnly(root, book) {
115
117
  try {
116
118
  const parsed = JSON.parse(await readFile(file, "utf8"));
117
119
  if (Array.isArray(parsed?.entries)) return parsed.entries.map(normalizePlotEntry).filter(Boolean);
118
- } catch { /* 不存在/损坏 → 试下一个位置 */ }
120
+ } catch (e) {
121
+ // v6.6.0:「不存在」与「损坏/读不动」在这里都只表示"试下一个位置",差别只有日志记得下。
122
+ // 本条是 JSON.parse 路径(解析错误的消息会回显文件内容片段),故只记固定标签 + 文件名,绝不带 e.message
123
+ if (e?.code !== "ENOENT") {
124
+ logEvent("config", { book, file, level: "warn", detail: "伏笔表读取失败(按无伏笔处理,继续试下一位置)" });
125
+ }
126
+ /* 不存在/损坏 → 试下一个位置 */
127
+ }
119
128
  }
120
129
  return [];
121
130
  }
@@ -126,6 +135,11 @@ async function readCreationFile(root, book, key, exec) {
126
135
  return await readTextFile(join(creationDir(root, book), CREATION_FILES[key]), exec);
127
136
  } catch (e) {
128
137
  if (isAbort(e)) throw e;
138
+ // v6.6.0:**读不到 ≠ 没写过**。调用方把 null 当作"这本书没有创作资料"(degraded 里会写"未初始化"),
139
+ // 于是权限/编码类失败会伪装成"还没建创作资料"——用户照提示去 init 也没用。ENOENT 不记(常态)。
140
+ if (e?.code !== "ENOENT") {
141
+ logEvent("config", { book, file: CREATION_FILES[key], level: "warn", detail: "创作资料读取失败(调用方按「没有创作资料」处理):" + (e?.message ?? e) });
142
+ }
129
143
  return null;
130
144
  }
131
145
  }
@@ -484,15 +498,24 @@ export async function buildChapterBrief(root, book, chapterArg, opts, exec) {
484
498
  }
485
499
  let anchors = [];
486
500
  let skeletons = [];
501
+ // v6.6.0:锚包的**生成失败**与**正文不满足抽样条件**(段落 ≥ 40 字)必须可区分。
502
+ // 旧版两者都落到下面那句「抽样要求段落 ≥ 40 字,本书正文不满足」——把系统的失败说成作者的正文太短 ✗。
503
+ // 判据来自 buildStyleAnchorPackage 的 failed 标记(只在失败路径出现,成功路径返回结构未变)。
504
+ let anchorUnavailable = false;
487
505
  try {
488
506
  const pkg = await buildStyleAnchorPackage(baseRoot, safeBook, anchorChapters, exec, anchorTexts);
489
507
  anchors = (pkg.anchors ?? []).slice(0, budget.anchors);
490
508
  skeletons = (pkg.skeletons ?? []).slice(0, budget.skeletons);
509
+ if (pkg?.failed === true) {
510
+ anchorUnavailable = true;
511
+ degraded.push("原著锚段/句式骨架生成失败(内部错误,本次抽样未完成)——与「正文段落不足 40 字」不是一回事,请查 novel_diagnose 的事件日志或重试");
512
+ }
491
513
  } catch (e) {
492
514
  if (isAbort(e)) throw e;
515
+ anchorUnavailable = true;
493
516
  degraded.push("原著锚段/句式骨架不可用:" + errText(e));
494
517
  }
495
- if (anchorChapters.length > 0 && anchors.length === 0 && skeletons.length === 0) {
518
+ if (anchorChapters.length > 0 && anchors.length === 0 && skeletons.length === 0 && !anchorUnavailable) {
496
519
  degraded.push("原著锚段与句式骨架为空(抽样要求段落 ≥ 40 字,本书正文不满足)");
497
520
  }
498
521