dsh-novel-writer 6.5.5 → 6.6.5
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 +23 -10
- package/README.md +18 -9
- package/cordis.patch.yml +1 -1
- package/lib/analysis.js +65 -11
- package/lib/brief.js +25 -2
- package/lib/client.js +275 -10
- package/lib/core.js +285 -38
- package/lib/diagnose.js +1057 -0
- package/lib/embedding.js +22 -3
- package/lib/index.js +335 -43
- package/lib/log.js +378 -0
- package/lib/vibe.js +23 -5
- package/mcp/README.md +7 -6
- package/package.json +2 -2
- package/server.json +3 -3
package/README.en.md
CHANGED
|
@@ -10,13 +10,13 @@ English | [中文](./README.md)
|
|
|
10
10
|
[](https://github.com/siweina/dsh-novel-writer/stargazers)
|
|
11
11
|
[](https://glama.ai/mcp/servers/siweina/dsh-novel-writer)
|
|
12
12
|
|
|
13
|
-
**
|
|
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
|
+
[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 | **
|
|
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,
|
|
@@ -172,18 +173,18 @@ fixAnchors:3 条原著锚段(对话 / 心理 / 描写各一条,供逐句
|
|
|
172
173
|
3. **12-axis vibe spectrum**: nightmare / angst / heartwarming / fluff / tearjerker / dark / mystery / blaze / absurd / lonesome / aesthetic / sensual — with traceable evidence, 0 token.
|
|
173
174
|
4. **Local semantic engine**: bge-small-zh Chinese model (24MB, shipped with the plugin) local CPU inference — `novel_semantic_search` finds semantically related passages with natural language (with chapter location), semantic style comparison, semantic implicit emotion; lazy loading + graceful fallback.
|
|
174
175
|
5. **Sentence-pattern analysis**: 9 categories, arrangement patterns, rhythm, emotion curve, style fingerprint + guidance, with cache & report export.
|
|
175
|
-
6. **Emotion purification & quantification**: strong/weak emotion-word grading, pollution detection, caveat warning + AI re-verification; Valence sliding window → variance V / delta Δ / conflict index C + implicit imagery carriers.
|
|
176
|
+
6. **Emotion purification & quantification**: strong/weak emotion-word grading, pollution detection, caveat warning + AI re-verification; Valence sliding window → variance V / delta Δ / conflict index C + implicit imagery carriers.<br>**Emotion complexity now follows confidence (v6.6.5)**: when the conclusion is not trustworthy (a pollution threshold is hit, the text contains no emotion words at all, or no usable clean counts remain) the tool no longer hands out a top-tier verdict — `complexity.level` is reported as `unknown`, together with `reliable: false` and a `reliabilityNote` giving the reason, and the rendered text prints that warning on its own line; `score` keeps its numeric value for cross-chapter comparison. The emotion dimension is simply not applicable to R18 / high-physiological prose — use rhythm, payoff and dialogue density instead.
|
|
176
177
|
7. **Worldview & pragmatics detection**: auto cultural-baseline detection with confidence; speechStyle title/honorifics/rituals/tone norms; genre & theme + webnovel signals.
|
|
177
178
|
8. **Writing toolkit**: plot tracking / five settings tables (characters·locations·items·timeline·worldview) / chapter summaries / continuity audit / batch import / style check / continuation writing.
|
|
178
179
|
9. **Per-tool UI toggles**: a 「写作助手功能」 ("Writing Assistant") entry in the left sidebar plus a main-column panel (**official DSH 0.2.0 seats**; hosts older than 0.2.0 fall back automatically) — master switch + grouped tool toggles + feature toggles + prompt mode/scene/**lean workflow**, plain-language labels, data-dir usage & semantic-engine status. All colors go through `--dsw-alias-*` semantic tokens (three themes in sync with the host), controls use the official UI primitives, DeepSeek-blue accents, a glow hero header with section kickers, and equal-width pages across every level.<br>**The UI goes further (Plan A)**: source split + build chain (assert-failed builds never write, `--check` verifies source==artifact), 6 unified components with a single row pattern (`PreferenceRow`), group cards across every switch page, 19+5 line icons, a slider as auxiliary input on the tolerance rows, and page-level push-pop transition animations (zero state / zero hook).
|
|
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 (
|
|
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
|
|
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
|
|
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;
|
|
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
|
[](https://github.com/siweina/dsh-novel-writer/stargazers)
|
|
11
11
|
[](https://glama.ai/mcp/servers/siweina/dsh-novel-writer)
|
|
12
12
|
|
|
13
|
-
**
|
|
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/) · [技术文档(
|
|
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 集成 | **
|
|
158
|
+
| 与 DSH 集成 | **20 工具 + 官方席位与官网式界面** | 无 | 无 |
|
|
158
159
|
| 非 DSH 用户可用 | **可以(MCP)** | 可以 | 需自己封装 |
|
|
159
160
|
|
|
160
161
|
> **致非中文用户**:本插件为中文小说分析写作而设计——句式、情感、意象等核心能力以及内置的语义模型,全部针对中文语料构建与调优。在深耕中文的同时兼顾英文等其他语言,确实超出了我目前的能力范围。若因此给您带来不便,我深感抱歉,恳请谅解。
|
|
@@ -168,18 +169,18 @@ fixAnchors:3 条原著锚段(对话 / 心理 / 描写各一条,供逐句
|
|
|
168
169
|
3. **氛围光谱 12 轴**:噩梦感 / 焦虑压抑 / 温馨治愈 / 甜宠日常 / 催泪虐心 / 黑暗残酷 / 悬疑神秘 / 热血激昂 / 荒诞无厘头 / 孤独疏离 / 文艺唯美 / 情欲暧昧——证据链可追溯,0 token。
|
|
169
170
|
4. **本地语义引擎**:bge-small-zh 中文模型(24MB 随插件分发)本地 CPU 推理——`novel_semantic_search` 自然语言搜全书语义相关段落(带章节定位),语义级风格对比、语义隐性情感,懒加载 + 自动回退。
|
|
170
171
|
5. **句式模式分析**:九类句式分布、排列规律、句长节奏、情感曲线、风格指纹与节奏建议,带缓存与报告导出。
|
|
171
|
-
6. **情感净化 + 量化**:强/弱情绪词分级、污染源检测、caveat 预警 + AI 复核;Valence 滑动窗口 → 方差 V / 斜率 Δ / 矛盾指数 C +
|
|
172
|
+
6. **情感净化 + 量化**:强/弱情绪词分级、污染源检测、caveat 预警 + AI 复核;Valence 滑动窗口 → 方差 V / 斜率 Δ / 矛盾指数 C + 隐性意象载体。<br>**情感复杂度与置信度联动(v6.6.5)**:结论不可采信时(污染命中阈值、全文零情感词命中,或没有可用的 clean 情感计数)不再给顶格结论——`complexity.level` 标为 `unknown`,并附 `reliable: false` 与 `reliabilityNote` 说明原因,渲染文本里也会单独一行写出这句警告;`score` 保留数值便于跨章对比。R18 / 高生理描写文本的情感维度本就不适用,建议改用节奏、爽点、对话密度等维度。
|
|
172
173
|
7. **世界观与语用检测**:文化基准自动判断(西/东/混合)+ 置信度;speechStyle 称谓/客套/仪式/语气规范;题材流派 + 网文信号。
|
|
173
174
|
8. **写作辅助全家桶**:伏笔登记表 / 设定五张表(人物·地点·道具·时间线·世界观)/ 章节摘要 / 连贯性审计 / 批量导入 / 风格自检 / 续写辅助。
|
|
174
175
|
9. **全工具 UI 开关**:左栏「写作助手功能」入口 + 主栏面板(**DSH 0.2.0 官方席位**;老宿主 < 0.2.0 自动回退旧挂载)——总开关 + 工具开关分组 + 功能开关 + 提示词档位/场景/**精简工作流**,大白话文案,显示数据目录占用与语义引擎状态。颜色全走 `--dsw-alias-*` 语义 token(三主题与官方同源)、控件用官方 UI primitives、品牌蓝强调、光晕头部与分区 kicker、各级页面等宽。<br>**界面再进一步(方案 A)**:源码分块 + 构建链装配(构建失败不写盘、`--check` 校验源与产物一致)、6 个统一组件与单一行模式(`PreferenceRow`)、分组卡片贯穿全部开关页、19+5 枚线图标、阈值 slider 辅助输入、页面级 push-pop 过渡动画(零 state / 零 hook)。
|
|
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
|
|
179
|
+
13. **体验与统计**:主面板**书库统计卡**(每本章数/总字数/近 7 天活跃字数,活跃🔥标绿)、**🎬 体验演示**(内置示例不落盘跑六维基线)、**📊 报告历史**(analysis/style-reports 列表浏览)、**🩺 诊断**(诊断报告 + 统计窗口 + **一键复制诊断包**,用于贴进 issue);错误提示带解决步骤;工具说明压缩省 token。
|
|
179
180
|
|
|
180
181
|
---
|
|
181
182
|
|
|
182
|
-
## 提供的工具(
|
|
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`),把
|
|
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 内注册
|
|
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:
|
|
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
|
@@ -1165,20 +1165,57 @@ export function chapterEmotionStats(counts) {
|
|
|
1165
1165
|
};
|
|
1166
1166
|
}
|
|
1167
1167
|
|
|
1168
|
-
/**
|
|
1169
|
-
|
|
1168
|
+
/**
|
|
1169
|
+
* v1.6.0 全书聚合:复杂度评分 0-1(熵归一化0.5 + 多样性0.25 + 主次冲突0.25)+ 章间漂移。
|
|
1170
|
+
*
|
|
1171
|
+
* v6.6.5 修正(用户反馈「第一章的情感复杂度会直接爆掉」):
|
|
1172
|
+
* 复杂度此前**与置信度完全脱钩** ✗ —— 同一份输出里一边写着 `confidence: "low"` + 「检测到高密度
|
|
1173
|
+
* R18/生理描写,dominant 可能来自生理/爽感词而非真实情感,请勿采信」,另一边**照样**给出
|
|
1174
|
+
* `complexity: { score: 0.6, level: "high" }`。下游 AI 只要读 `level` 就会得出「情感复杂度高」的
|
|
1175
|
+
* 结论,**而这个结论已被工具自己否定**。实测(棍妹 第一/二章,R18 密度 8.99‰ / 9.77‰):
|
|
1176
|
+
* 两章都判 `level: "high"`,而两章的 `confidence` 都是 `"low"`。
|
|
1177
|
+
*
|
|
1178
|
+
* 现在接收 `reliability`(由调用方把污染/无数据判定传进来),当结论不可采信时:
|
|
1179
|
+
* · `level` 降级为 `"unknown"`(**不再给顶格的 high**)
|
|
1180
|
+
* · 加 `reliable: false` + `reliabilityNote`(固定说明)—— 让"不可采信"出现在**结论字段本身**,
|
|
1181
|
+
* 而不是只写在旁边的 caveat 里(那正是误导的来源)
|
|
1182
|
+
* · `score` 数值保留(便于对比),但 `reliable: false` 已标明其地位
|
|
1183
|
+
*
|
|
1184
|
+
* @param perChapter 分章计数(**clean 计数**,调用方已用 cleanScoresOf 聚合)
|
|
1185
|
+
* @param reliability 可选:{ polluted?: boolean, pollutedBy?: string[], noData?: boolean }
|
|
1186
|
+
*/
|
|
1187
|
+
export function emotionComplexity(perChapter, reliability) {
|
|
1188
|
+
const rel = reliability && typeof reliability === "object" ? reliability : null;
|
|
1189
|
+
const polluted = rel?.polluted === true;
|
|
1190
|
+
const noData = rel?.noData === true;
|
|
1191
|
+
const unreliable = polluted || noData;
|
|
1192
|
+
const reliabilityNote = polluted
|
|
1193
|
+
? "⚠️ 本复杂度基于已被污染的情感计数(" + ((rel?.pollutedBy ?? []).join("、") || "高密度场景性用词") + "),不可采信:请勿据此判断文本的情感复杂度,须 novel_read 抽查原文后自行判断。"
|
|
1194
|
+
: noData
|
|
1195
|
+
? "ℹ️ 未检出任何情感词,本复杂度无数据可依(score 0 表示「没测到」,不是「情感简单」),请勿据此下结论。"
|
|
1196
|
+
: "";
|
|
1197
|
+
const levelOf = (raw) => (unreliable ? "unknown" : raw);
|
|
1170
1198
|
// v4.0.0 修正:c.counts 缺失时不再抛 TypeError(与其余 ?? 0 的防御风格一致)
|
|
1171
1199
|
const list = Array.isArray(perChapter) ? perChapter : [];
|
|
1172
1200
|
const stats = list.map((c) => ({ chapter: c?.chapter, ...(chapterEmotionStats(c?.counts) ?? {}) })).filter((s) => s.entropy !== void 0);
|
|
1173
1201
|
if (stats.length === 0) {
|
|
1174
|
-
//
|
|
1202
|
+
// v6.6.5 修正(**由文档负责人在实测中独立发现的同类残留**,与本次要修的是同一类问题):
|
|
1203
|
+
// 走到这里说明 **clean 情感计数为空**(stats 正是从 clean 计数算出来的)—— 也就是复杂度
|
|
1204
|
+
// **无数据可依** ✗。而此前只在 `noData`(**raw** 计数为零)时才标不可采信,于是
|
|
1205
|
+
// 「raw 命中了弱词、clean 一个强词都没有」这类文本(实测 `她笑了笑,眼泪掉了下来。`)
|
|
1206
|
+
// 会得到 `level: "low"` + `reliable: true`,**而 `reliabilityNote` 却写着「无数据可依」** ✗
|
|
1207
|
+
// —— 结论与说明自相矛盾,正是本版要消灭的形态。现在统一:**没有 clean 数据 ⇒ 不可采信、level 给 unknown**。
|
|
1175
1208
|
return {
|
|
1176
|
-
score: 0, level: "
|
|
1209
|
+
score: 0, level: "unknown", entropy: 0, maxEntropy: Math.round(EMOTION_MAX_ENTROPY * 1000) / 1000,
|
|
1177
1210
|
// v4.3.0(清单第 35 条):无次要情感时 secondary 统一为空串(旧版此处"neutral"、
|
|
1178
1211
|
// 上方非空分支却给"joy"这类 0 占比情感,两处口径不同)。
|
|
1179
1212
|
diversity: 0, dominant: "neutral", dominantRatio: 0, secondary: "", secondaryRatio: 0,
|
|
1180
1213
|
conflict: "", conflictStrength: 0,
|
|
1181
|
-
chapterDrift: { meanEntropy: 0, entropyVariance: 0, swinging: false }
|
|
1214
|
+
chapterDrift: { meanEntropy: 0, entropyVariance: 0, swinging: false },
|
|
1215
|
+
reliable: false,
|
|
1216
|
+
reliabilityNote: polluted
|
|
1217
|
+
? reliabilityNote
|
|
1218
|
+
: "ℹ️ 未检出可用的情感计数(clean 计数为空),本复杂度无数据可依:score 0 表示「没测到」,不是「情感简单」,请勿据此下结论。"
|
|
1182
1219
|
};
|
|
1183
1220
|
}
|
|
1184
1221
|
const totalCounts = {};
|
|
@@ -1196,7 +1233,8 @@ export function emotionComplexity(perChapter) {
|
|
|
1196
1233
|
const level = score >= 0.6 ? "high" : score >= 0.4 ? "medium" : "low";
|
|
1197
1234
|
return {
|
|
1198
1235
|
score: Math.round(score * 100) / 100,
|
|
1199
|
-
|
|
1236
|
+
// v6.6.5:不可采信时降级为 "unknown" —— 见函数头说明(原先照样给 high,与 confidence:"low" 自相矛盾)
|
|
1237
|
+
level: levelOf(level),
|
|
1200
1238
|
entropy: global.entropy,
|
|
1201
1239
|
maxEntropy: Math.round(EMOTION_MAX_ENTROPY * 1000) / 1000,
|
|
1202
1240
|
diversity: global.diversity,
|
|
@@ -1212,14 +1250,19 @@ export function emotionComplexity(perChapter) {
|
|
|
1212
1250
|
meanEntropy: Math.round(meanEntropy * 1000) / 1000,
|
|
1213
1251
|
entropyVariance: Math.round(entropyVariance * 10000) / 10000,
|
|
1214
1252
|
swinging: entropyVariance > 0.03
|
|
1215
|
-
}
|
|
1253
|
+
},
|
|
1254
|
+
reliable: !unreliable,
|
|
1255
|
+
reliabilityNote
|
|
1216
1256
|
};
|
|
1217
1257
|
}
|
|
1218
1258
|
|
|
1219
1259
|
/**
|
|
1220
1260
|
* v1.6.0 情感量化入口:三指标 + 显隐对比 + 复杂度 + 复合共现(纯规则 0 token)。
|
|
1261
|
+
*
|
|
1262
|
+
* v6.6.5:新增第 4 个可选参数 `reliability`,透传给 `emotionComplexity`——
|
|
1263
|
+
* 让复杂度知道"这次的情感计数是否被污染/是否根本没数据",从而不再给出与 `confidence` 矛盾的顶格结论。
|
|
1221
1264
|
*/
|
|
1222
|
-
export function emotionalQuantification(text, perChapter, blocks) {
|
|
1265
|
+
export function emotionalQuantification(text, perChapter, blocks, reliability) {
|
|
1223
1266
|
// v4.3.0(清单第 33 条):入参兜底——旧版 emotionalQuantification("x", [{chapter,counts}])
|
|
1224
1267
|
// 抛 "blocks is not iterable",而它下面的 fallback 分支本意就是支持"无分章数据"这一输入。
|
|
1225
1268
|
const src = String(text ?? "");
|
|
@@ -1242,7 +1285,7 @@ export function emotionalQuantification(text, perChapter, blocks) {
|
|
|
1242
1285
|
stats,
|
|
1243
1286
|
implicit,
|
|
1244
1287
|
compare: explicitImplicitCompare(meanValence, implicit),
|
|
1245
|
-
complexity: emotionComplexity(perChapter),
|
|
1288
|
+
complexity: emotionComplexity(perChapter, reliability),
|
|
1246
1289
|
composites: compositeEmotionPairs(blockList)
|
|
1247
1290
|
};
|
|
1248
1291
|
}
|
|
@@ -1537,7 +1580,13 @@ export function analyzeText(text, options = {}) {
|
|
|
1537
1580
|
}).filter((m) => Object.values(m.counts).some((v) => v > 0))
|
|
1538
1581
|
};
|
|
1539
1582
|
})();
|
|
1540
|
-
const quantification = emotionalQuantification(sampleText, perChapterGroups.entries, blockMeta.map((m) => m.sentences).filter((s) => s.length > 0)
|
|
1583
|
+
const quantification = emotionalQuantification(sampleText, perChapterGroups.entries, blockMeta.map((m) => m.sentences).filter((s) => s.length > 0), {
|
|
1584
|
+
// v6.6.5:把"这次的情感计数可不可信"传给复杂度,避免出现
|
|
1585
|
+
// 「confidence: low(勿采信)」与「complexity.level: high」并存的矛盾输出。
|
|
1586
|
+
polluted,
|
|
1587
|
+
pollutedBy,
|
|
1588
|
+
noData: totalEmotion === 0
|
|
1589
|
+
});
|
|
1541
1590
|
if (quantification?.complexity?.chapterDrift) quantification.complexity.chapterDrift.basis = perChapterGroups.basis;
|
|
1542
1591
|
const emotion = {
|
|
1543
1592
|
dominant: dominantEmotion,
|
|
@@ -1836,6 +1885,8 @@ export function densityOf(text) {
|
|
|
1836
1885
|
// 懒加载 + 内存缓存;七类 → 五情感映射,供 sentimentCounts 扩展计数(词表未覆盖的词也能计分)
|
|
1837
1886
|
import { createRequire } from "node:module";
|
|
1838
1887
|
import { gunzipSync } from "node:zlib";
|
|
1888
|
+
// v6.6.0:事件日志(log.js 无依赖,可安全被本模块导入)
|
|
1889
|
+
import { logEvent } from "./log.js";
|
|
1839
1890
|
const __req = createRequire(import.meta.url);
|
|
1840
1891
|
let DUTIR = null;
|
|
1841
1892
|
function loadDutir() {
|
|
@@ -1844,8 +1895,11 @@ function loadDutir() {
|
|
|
1844
1895
|
// v4.0.1:dutir 词表以 gzip 随包分发(原文件 383KB 超出商店单文件上限),加载时解压
|
|
1845
1896
|
const p = __req.resolve("./lexicons/dutir_seven.json.gz");
|
|
1846
1897
|
DUTIR = JSON.parse(gunzipSync(__req("node:fs").readFileSync(p)).toString("utf8"));
|
|
1847
|
-
} catch {
|
|
1898
|
+
} catch (e) {
|
|
1848
1899
|
DUTIR = {};
|
|
1900
|
+
// v6.6.0:内置 27k 词七类情感表加载失败 → 整个 dutir 兜底扫描静默失效
|
|
1901
|
+
//(情感统计退回只有主词表,结果数字变小而没人知道为什么)。只随包分发,故基本是打包/权限问题。
|
|
1902
|
+
logEvent("config", { file: "lib/lexicons/dutir_seven.json.gz", level: "warn", detail: "内置七类情感词表加载失败,dutir 兜底扫描失效:" + (e?.message ?? e) });
|
|
1849
1903
|
}
|
|
1850
1904
|
return DUTIR;
|
|
1851
1905
|
}
|
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
|
|