给中文网文作者的本地写作工作台(20 个工具):动笔前取齐上一章承接口、大纲、人物卡、待回收伏笔、用语规范;写完后按六维文笔基线对照原著波动带自检,排出带原句行号的改稿待办;跨章查伏笔埋设跨度、人物连续缺席与大纲偏离…
数据截至 2026/11/2 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
git clone https://github.com/siweina/dsh-novel-writer.git状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
安装兼容性检查✓ 自动检查通过展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/30 04:33:11
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
查看扫描证据(共 210 处,展示前 87 条)
- 读取本地文件lib/index.js:86
content: stripSkillFrontmatter(await readFile(SKILL_BODY_URL, "utf8")) - 读取本地文件lib/index.js:181
const entries = await readdir(dir, { withFileTypes: true }); - 读取本地文件lib/index.js:185
try { files.push({ name: entry.name, mtime: (await stat(join(dir, entry.name))).mtimeMs }); } catch { } - 删除本地文件lib/index.js:190
try { await rm(join(dir, stale.name), { force: true }); } catch { } - 读取本地文件lib/index.js:282
srcInfo = await stat(src); - 读取本地文件lib/index.js:296
info = await stat(full); - 写入本地文件lib/index.js:390
await mkdir(destDir, { recursive: true }); - 读取本地文件lib/index.js:402
for (const ex of readdirSync(destDir)) { - 写入本地文件lib/index.js:411
await copyFile(srcFull, destFull); - 删除本地文件lib/index.js:412
if (move) await rm(srcFull, { force: true }); - 删除本地文件lib/index.js:534
await rm(join(dir, f.name), { force: true }); - 写入本地文件lib/index.js:899
await mkdir(cacheDir, { recursive: true }); - 写入本地文件lib/index.js:2649
await mkdir(reportDir, { recursive: true }); - 写入本地文件lib/index.js:3199
await mkdir(dirname(summariesFile(root, book)), { recursive: true }); - 写入本地文件lib/index.js:4041
await mkdir(auditDir, { recursive: true }); - 删除本地文件lib/index.js:5061
try { await rm(filePath, { force: true }); } catch { } - 读取本地文件mcp/server.mjs:85
const pkg = JSON.parse(readFileSync(join(PLUGIN_DIR, "package.json"), "utf8")); - 读取本地文件mcp/server.mjs:344
while (!existsSync(current)) { - 读取本地文件lib/brief.js:206
const parsed = JSON.parse(await readFile(file, "utf8")); - 读取本地文件lib/fixplan.js:432
text = await readFile(join(creationDir(root, book), CREATION_FILES.outline), "utf8"); - 读取本地文件lib/fixplan.js:489
const parsed = JSON.parse(await readFile(file, "utf8")); - 写入本地文件lib/fixplan.js:1440
await mkdir(dirname(planFile), { recursive: true }); - 读取本地文件lib/graph.js:157
raw = await readFile(file, "utf8"); - 读取本地文件lib/graph.js:182
raw = await readFile(file, "utf8"); - 读取本地文件lib/graph.js:740
if (!existsSync(dir)) { - 读取本地文件lib/core.js:380
return existsSync(join(root, "novels")) || existsSync(join(root, ".novel-writer")); - 读取本地文件lib/core.js:463
entries = await readdir(bookPath, { withFileTypes: true }); - 读取本地文件lib/core.js:483
const buf = await readFile(filePath, { signal: exec?.signal }); - 读取本地文件lib/core.js:628
const info = await stat(join(bookPath, chapter.file)); - 读取本地文件lib/core.js:791
for (const ex of readdirSync(destDir)) { - 读取本地文件lib/core.js:804
if (!existsSync(join(destDir, fileName))) return fileName; - 写入本地文件lib/core.js:1305
await writeFile(tmp, JSON.stringify(obj, null, 2), "utf8"); - 写入本地文件lib/core.js:1307
await rename(tmp, file); - 写入本地文件lib/core.js:1315
await writeFile(file, JSON.stringify(obj, null, 2), "utf8"); - 删除本地文件lib/core.js:1317
try { await rm(tmp, { force: true }); } catch { } - 删除本地文件lib/core.js:1324
try { await rm(tmp, { force: true }); } catch { } - 写入本地文件lib/core.js:1377
await mkdir(dirname(file), { recursive: true }); - 执行外部命令固定命令lib/core.js:1485
execFile("explorer.exe", ["/select," + dir], function (err) { resolve(!explorerSpawnFailed(err)); }); - 执行外部命令固定命令lib/core.js:1493
execFile("open", [dir], function (err) { resolve(!err); }); - 执行外部命令固定命令lib/core.js:1499
execFile("xdg-open", [dir], function (err) { resolve(!err); }); - 写入本地文件lib/core.js:1951
await mkdir(dir, { recursive: true }); - 写入本地文件lib/core.js:2188
if (bnext !== bcur) await writeFile(bf, bnext, "utf8"); - 删除本地文件递归lib/core.js:2206
if (safe4) await rm(d4, { recursive: true, force: true }); - 删除本地文件递归lib/core.js:2243
if (safeToDelete) await rm(dir3, { recursive: true, force: true }); - 读取本地文件lib/log.js:233
try { size = statSync(logFilePath()).size; } catch { return; } - 删除本地文件lib/log.js:236
rmSync(rotatedPath(LOG_KEEP_FILES), { force: true }); - 读取本地文件lib/log.js:238
if (existsSync(rotatedPath(n))) renameSync(rotatedPath(n), rotatedPath(n + 1)); - 写入本地文件lib/log.js:238
if (existsSync(rotatedPath(n))) renameSync(rotatedPath(n), rotatedPath(n + 1)); - 写入本地文件lib/log.js:240
renameSync(logFilePath(), rotatedPath(1)); - 读取本地文件lib/log.js:275
if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); - 写入本地文件lib/log.js:275
if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); - 写入本地文件lib/log.js:277
appendFileSync(logFilePath(), line + "\n", "utf8"); - 读取本地文件lib/log.js:321
try { text = readFileSync(file, "utf8"); } catch { continue; } - 读取本地文件lib/log.js:366
const st = statSync(file); - 删除本地文件lib/log.js:379
try { rmSync(file, { force: true }); } catch { } - 读取本地文件lib/diagnose.js:143
return existsSync(join(path, "novels")) || existsSync(join(path, ".novel-writer")); - 读取本地文件lib/diagnose.js:163
path: dir, exists: existsSync(dir), bytes: 0, files: 0, subdirs: [], - 读取本地文件lib/diagnose.js:172
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { out.missingSubdirs = DATA_SUBDIRS.slice(); return out; } - 读取本地文件lib/diagnose.js:191
if (names === void 0) { try { names = readdirSync(bucket.path); } catch { names = []; } } - 读取本地文件lib/diagnose.js:195
try { st = statSync(full); } catch { continue; } - 读取本地文件lib/diagnose.js:213
try { buf = readFileSync(full); } catch { - 读取本地文件lib/bookdelete.js:136
const entries = await readdir(novelsDir(root), { withFileTypes: true }); - 读取本地文件lib/bookdelete.js:166
try { st = await stat(p); } catch { return out; } - 读取本地文件lib/bookdelete.js:176
try { entries = await readdir(dir, { withFileTypes: true }); } catch { return; } - 读取本地文件lib/bookdelete.js:182
const s = await stat(full); - 读取本地文件lib/bookdelete.js:212
if (existsSync(chapterDir)) { - 读取本地文件lib/bookdelete.js:221
if (existsSync(creationBookDir)) { - 写入本地文件lib/bookdelete.js:382
await mkdir(trashRoot, { recursive: true }); - 写入本地文件lib/bookdelete.js:439
await writeFile(join(trashRoot, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8"); - 删除本地文件递归lib/bookdelete.js:460
try { await rm(trashRoot, { recursive: true, force: true }); } catch { } - 删除本地文件递归lib/bookdelete.js:661
try { await rm(dir, { recursive: true, force: true }); } catch { } - 写入本地文件lib/bookdelete.js:665
await writeFile(join(dir, "manifest.json"), JSON.stringify({ ...manifest, items: stillThere }, null, 2), "utf8"); - 删除本地文件递归lib/bookdelete.js:685
await rm(dir, { recursive: true, force: true }); - 读取本地文件lib/update-check.js:89
if (!existsSync(p)) return null; - 读取本地文件lib/update-check.js:90
return JSON.parse(readFileSync(p, "utf8")); - 写入本地文件lib/update-check.js:99
mkdirSync(cacheDir, { recursive: true }); - 写入本地文件lib/update-check.js:100
writeFileSync(join(cacheDir, fileName), JSON.stringify(payload, null, 2), "utf8"); - 发起网络请求lib/update-check.js:212
const res = await fetch(GITHUB_API_URL, { - 写入本地文件lib/trash.js:51
await mkdir(dirname(to), { recursive: true }); - 写入本地文件lib/trash.js:53
await rename(from, to); - 删除本地文件递归lib/trash.js:57
await rm(from, { recursive: true, force: true }); - 读取本地文件lib/trash.js:73
const list = (Array.isArray(paths) ? paths : []).filter((p) => typeof p === "string" && p !== "" && existsSync(p)); - 读取本地文件lib/trash.js:81
for (let n = 2; existsSync(join(base, trashId)); n += 1) trashId = safe + "-" + stamp + "-" + n; - 写入本地文件lib/trash.js:84
await mkdir(trashRoot, { recursive: true }); - 读取本地文件lib/trash.js:96
const s = await stat(from); - 删除本地文件递归lib/trash.js:109
try { await rm(trashRoot, { recursive: true, force: true }); } catch { } - 写入本地文件lib/trash.js:116
await writeFile(join(trashRoot, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
功能亮点
- 本地章节体检
- 句式情感风格分析
- 本地模型零API花费
展开完整介绍(198 字)↓
给中文网文作者的本地写作工作台(20 个工具):动笔前取齐上一章承接口、大纲、人物卡、待回收伏笔、用语规范;写完后按六维文笔基线对照原著波动带自检,排出带原句行号的改稿待办;跨章查伏笔埋设跨度、人物连续缺席与大纲偏离;出错时可生成只含元数据、可安全分享的诊断报告。24MB 中文模型随包本地推理,零 token、正文不出本机。DSH 本地网文写作工作台:20 个工具,端侧模型,零 API 成本。
README
由 DeepSeek 最新模型翻译生成展开完整文档(README 剩余 1 节)
📚 dsh-novel-writer — 给网文作者的本地写作工作台
siweina/dsh-novel-writer MCP server
21 个工具,覆盖「动笔前取材料 → 写完量化自检 → 改稿按优先级复测 → 跨章结构体检 → 收尾清理与诊断」整条流程。 动笔前一次调用取齐 16 项材料(漏读一样就漂移);改稿不再只丢给你一堆数字,而是按严重度排好的待办(带原句行号、当前值与目标值、原著锚段,只给方向不代写);跨章层面能查「线断了、人丢了、伏笔忘了」。
句式 / 情感 / 语义全部在本地算:24MB 中文模型随包分发,零 API 花费、正文不出本机。
为 DeepSeek Harness(DSH)打造;同一套能力也可作为 MCP 服务器给 Claude Desktop / Cursor 使用。
官网 · 技术文档(21 个工具的参数与示例) · 安装 · 写作流程 · 60 秒上手 · 看看输出 · 21 个工具 · MCP 服务器
完整版本变更见 CHANGELOG.md。
写作流程(一条能走完的闭环)
| 步骤 | 你说什么 | 用哪个工具 | 拿到什么 |
| ① 取材料 | 「给下一章取材料」 | novel_chapter_brief | 一次调用取齐 16 项输入:上一章结尾原文(承接口)/ 上一章钩子 / 本章大纲方向 / 相关人物卡 / 待回收伏笔(含埋了多久)/ 世界观用语规范与禁词 / 六维基线 / 原著锚段与句式骨架 / 上一章自检结论 / 禁用清单 / 开写清单——缺哪项、为什么、怎么补写在 degraded 里 |
| ② 动笔 | 「写第 N 章」 | novel_new_chapter · write | 新章文件(自动附基线 μ 摘要与原著锚包);锚段只用来校准语感(不照骨架造句),数字只做事后校验 |
| ③ 自检 | 「自检这一章」 | novel_style_check · novel_sentence_analysis | 相似度 + 偏差清单 + 六维对照(带内 ✓ / 出带 ⚠)。只有偏离明显(≥2 倍容差)或读起来确实别扭时才改——单一维度轻微出带属正常波动,不要为对齐数字改文 |
| ④ 改稿 | 「把这章排成改稿清单」 | novel_fix_plan | 按「严重度 → 难度 → 行号」排序的待办(默认上限 12 条),每条带原句行号、当前值/目标值、锚段与改写方向;分批改(同段同类合并),改完最多 verify 一次 |
| ⑤ 跨章体检 | 「看看全书结构」 | novel_plot { action: "graph" } | 伏笔埋设跨度 / 人物连续缺席区间 / 剧情线最大空档 / 时间线顺序 / 大纲方向与正文的偏离 |
全程本地、只读优先:开写包与结构视图不写任何文件;改稿台只写自己的清单文件、从不改正文,也不代写正文;语义检索与情感分析零 token。
它解决什么问题
| 你的困扰 | 这里给的答案 |
| "我续写的这段,读起来不像我自己写的" | 文笔六维基线:从句法复杂度 / 修饰密度 / 抽象度 / 动作密度 / 不确定性 / 留白指数六个维度算出原著的 μ±σ,新章逐维对照,出带标 ⚠ |
| "AI 接着往下写,几章之后文风越来越像 AI 自己,不像原著了" | 基线范围(按全书 / 按部分):基线不必再从全书取样——勾出你认作标杆的那几章(比如你亲手写的前 30 章),自检 / 开写包 / 改稿台 / 风格画像 / 建章都只用这几章当基准,正在检查的那一章永远排除在外。插件不判定「你漂移了」,只告诉你「相对你选的标杆,这章偏了多少」(回应 issue #1「风格漂移」) |
| "AI 说文风变了,但说不清哪儿变了" | 风格自检:相似度 + 偏差清单(哪类句式多了、句长偏了多少、主导情绪有没有换) |
| “动笔前要翻六七个工具,还老漏读一样” | 开写包:一次调用取齐 16 项材料;取不到的在 degraded 里说明缺什么、怎么补——漏读正是文风漂移与设定矛盾的头号来源 |
| “知道这章有问题,但不知道先改哪句” | 改稿台:按「严重度 → 难度 → 行号」排好待办,每条给原句行号 / 当前值与目标值 / 原著锚段 / 改写方向;改完可三态复测 |
| “线断了、人丢了、伏笔忘了——写到后面才崩” | 结构视图:跨章算出伏笔埋设跨度、人物连续缺席区间、剧情线最大空档、时间线顺序、大纲与正文的偏离 |
| “分析小说要花钱调 API” | 语义检索与情感分析全本地推理,零 token 花费 |
| “伏笔埋了忘了收” | 伏笔登记表:add / list / scan / done,自动记录每条伏笔在哪些章被提到 |
| “人物设定前后打架” | 设定五张表 + 连贯性审计(衔接 / OOC / 大纲走偏三件套) |
| “报告看不懂” | 全是表格化数字 + 原文锚段,可以直接截图分享 |
| “出错了只给一句话,说不清为什么反复出错” | 诊断报告(novel_diagnose):读本地事件日志 + 五处体检(书库根 / 书库内容 / 数据目录损坏 / 语义引擎 / 开关快照),聚合「工具 × 书名」反复出问题的组合、高频失败类型与可执行建议;报告走分享层(自由文本整体剔除、错误一律归类成固定 code),可安全贴进 issue——报 bug 请用面板「复制诊断包」或本工具的 report,不要直接附本机日志文件。本地日志以元数据为主(工具名 / 书名 / 章号 / 状态 / 耗时 / 计数 / 路径 / 错误 code),但自由文本字段(错误消息等)经收敛后至多保留一句(≤160 字)——所以本机日志里可能出现被错误消息回显的输入片段;日志文件不出本机,只有离开本机的内容才整体剔除自由文本 |
| “放弃一本书 / 删测试项目,却清理不干净” | 按书删除(novel_delete_book):一本书的资料散在 12 处,这里一处预览、一处清理;按「前缀 + 分隔符边界」匹配文件名(删《孤舟》不会碰到《孤舟子》)。默认只预览,执行时移入回收站(可恢复,同名文件绝不覆盖),要真省空间再 purge |
安装
方式一:npm(推荐)
dsh plugin add dsh-novel-writer
(需要指定配置时加 --profile <名称>,如 --profile desktop。)
方式二:从 GitHub 安装
dsh plugin add github:siweina/dsh-novel-writer#main
方式三:MCP(不用 DSH 也能用) —— 见 MCP 服务器
要求 Node ≥ 22.3。安装后重启 DSH(桌面端 / Web),左栏主导航区出现「写作助手功能」入口(DSH 0.2.0+ 官方席位;更早宿主自动回退旧挂载)。
60 秒上手
mkdir -p novels/我的小说 # 把章节文件放进去(第01章.md、第02章.md …)
然后在对话里依次说(这就是完整的一章):
1. 「给下一章取材料」 → novel_chapter_brief 一次给齐 16 项输入(等于替你跑了 6~8 个工具,且不会漏读)
2. 「写第 7 章」 → 照返回的锚段与骨架写;novel_new_chapter 建文件时还会附上基线 μ
3. 「自检第 7 章」 → novel_style_check 给出相似度、偏差清单与六维对照
4. 「把第 7 章排成改稿清单」 → novel_fix_plan 给出按优先级排好的待办;改完再 verify 复测三态
想让基线对着你亲手写的那些章、而不是全书?面板「风格基线」里把「范围」切到 按部分、在「挑选章节」里勾出标杆章,再点「保存范围」即可:上面这条链上的自检、开写包、改稿台与风格画像都会改用这几章当基线(按书生效;默认「全书」=旧行为,不动它就跟以前完全一样)。建章(novel_new_chapter)时附的 μ 摘要与锚段用的是同一份范围。
只想先看看家底,就说 「用 novel_style_report 给我的小说做一次风格画像」:
全书 1329 字:六维基线 μ=句法复杂度:2.3 修饰密度:35.6 抽象度:0.5 动作密度:101.7 不确定性:2.1 留白指数:7.0 推荐容差 25%/35%/100%…
看看输出长什么样
开写包(novel_chapter_brief,动笔前一次调用):
目标:第 8 章《(无标题)》(尚未创建,文件名推导为 第08章.md)
【上一章结尾原文·承接口】…(上一章末尾 300 / 600 字,按 budget 档位)【待回收伏笔】
- [mu5kjla…] 琥珀色齿轮怀表的来历与停摆的指针(high|第 1 章埋下,已过 6 章)
- [mu5kjlb…] 海图上红铅笔圈的礁区坐标(high|第 3 章埋下,已过 4 章)
【相关人物】- 林昭:守码头的女人,父亲失踪后回到旧宅 - 沈砚:随船出海的人【世界观用语】欧式中世纪沿海城邦;点烛不烧香|禁用:上香、烧香、时辰、老夫【风格基线】complexity μ=2.42 modifierDensity μ=22.2 abstractDensity μ=10.06 actionDensity μ=122.45 …(取样范围:全书除本章外的 7 章)
【原著锚段·照这个味道写】[对话] … [心理] …
【上一章自检】第 7 章六维对照:全部维度在容差带内 ✓ 【本章禁用清单】- 禁词:上香(建议改用:点烛) - 禁词:时辰(建议改用:钟点)【开写清单】□ 先读锚段再动笔 □ 承接上一章结尾 □ …(共 8 步)【降级/提示】- 钩子记录里没有第 7 章的钩子(上一章钩子未回填)
改稿台(novel_fix_plan,把诊断变成排好序的待办):
改稿台:共 13 项待办(严重度降序 → 难度升序)。抽象度过高 ×3、衔接缺失 ×1、禁用词 ×3、语用不符 ×5、句式偏离 ×1。
备注:留白指数 虽出带但差值 8.36 < 门槛 12,已按「无量级差异」忽略 ← 不误报的绝对量级闸
- [抽象度过高] 严重度 5 / 难度 2(第 5 行)
id:fix-abstract-5-c2c156d4 现状:abstractDensity 47.9 目标:0.55~6.95 原句:林昭大概说不清那种感觉。她隐隐觉得…
方向:抽象度偏离:全章 47.9(基线 3.75,偏差 +1177.3%),本段 74.38。这句抽象词过密,改成具体动作或物件。
锚段:林昭把它捏在掌心,翻过来看背面。背面刻着一行小字,被磨得只剩半边。她认出了父亲的名字。
- [语用不符] 严重度 4 / 难度 2(第 15 行)
现状:客套禁词「承蒙」(第 15 行) 目标:避免该类客套表达方向:「承蒙」属世界观说话方式规范里明确不用的客套表达(honorBad)。这是说法层面的替换,不要顺手改剧情。
风格自检(新章 vs 全书基线):
相似度 0.946 · verdict: high 偏差清单:心理占比略多 · 对话占比略少 · 短句占比略少 · 主导情绪由 anger 变为 joy fixAnchors:3 条原著锚段(对话 / 心理 / 描写各一条,供逐句对照修正)
语义检索(自然语言,本地向量):
查询「与那盏没有点的灯有关的段落」→ 第02章.md 0.619 对街那盏灯,亮了。
第01章.md 0.593 阿澈的目光越过老周的肩膀,落在对街那栋小楼上…
段落结构:共 34 段(对话 5 / 心理 0 / 混合 15 / 叙述 14)
为什么不用在线 AI 写作工具
| | 本插件 | 在线 AI 写作工具 | 通用文本分析库 |
| 正文是否离开本机 | 否 | 是 | 视实现 |
| 花费 | 0(本地推理) | 按 token 计费 | 自建 |
| 中文小说专用 | 是 | 通用 | 否 |
| 风格基线(μ±σ) | 有 | 少见 | 无 |
| 与 DSH 集成 | 21 个工具 + 官方席位与统一面板 | 无 | 无 |
| 非 DSH 用户可用 | 可以(MCP) | 可以 | 需自己封装 |
致非中文用户:本插件为中文小说分析写作而设计——句式、情感、意象等核心能力以及内置的语义模型,全部针对中文语料构建与调优。在深耕中文的同时兼顾英文等其他语言,确实超出了我目前的能力范围。若因此给您带来不便,我深感抱歉,恳请谅解。
功能
1. 写作台三件套:novel_chapter_brief 开写包——动笔前一次调用取齐材料(上一章承接口 / 本章方向 / 相关人物 / 待回收伏笔 / 用语规范 / 风格基线 / 锚段与骨架 / 禁用清单 / 开写清单,两档 budget:compact / full),取不到的材料进 degraded 并说明原因,只读不写盘;novel_fix_plan 改稿台——把风格诊断变成按优先级排好的待办(原句行号定位 + 当前值与目标值 + 原著锚段 + 改写方向),plan / verify / mark 三态复测,只给方向、不生成正文;novel_plot { action: "graph" } 结构视图——伏笔埋设跨度 / 人物连续缺席 / 剧情线空档 / 时间线顺序 / 大纲对照。另有场景化提示词(general / writing / revising / auditing / setup,general 即旧行为)。
2. 风格画像报告(novel_style_report):6 维测量报告——文风指纹 / 高频词汇 / 题材流派 / 情感量化 / 氛围光谱 12 轴 / 语义风格距离。测量与判断分离:插件只报数不贴标签,AI 判断可回传存盘(.novel-writer/style-reports/),续写保持风格一致。
3. 氛围光谱 12 轴:噩梦感 / 焦虑压抑 / 温馨治愈 / 甜宠日常 / 催泪虐心 / 黑暗残酷 / 悬疑神秘 / 热血激昂 / 荒诞无厘头 / 孤独疏离 / 文艺唯美 / 情欲暧昧——证据链可追溯,0 token。
4. 本地语义引擎:bge-small-zh 中文模型(24MB 随插件分发)本地 CPU 推理——novel_semantic_search 自然语言搜全书语义相关段落(带章节定位),语义级风格对比、语义隐性情感,懒加载 + 自动回退。
5. 句式模式分析:九类句式分布、排列规律、句长节奏、情感曲线、风格指纹与节奏建议,带缓存与报告导出。细节密度口径(v7.0.0):动作 / 物件 / 视觉 / 听觉 / 嗅觉 / 触觉 / 温度七个通道的词表加了上下文豁免层——「一点」「根本」「一样」「痛苦」「干净」「温柔」这类虚词搭配不再计数(同一章实测降约 15%~20%),报告会多渲染一行口径说明;同一章的新旧数值不可直接比较,跨章比较只用同一口径的数值。
6. 情感净化 + 量化:强/弱情绪词分级、污染源检测、caveat 预警 + AI 复核;Valence 滑动窗口 → 方差 V / 斜率 Δ / 矛盾指数 C + 隐性意象载体。情感复杂度与置信度联动(v6.6.5):结论不可采信时(污染命中阈值、全文零情感词命中,或没有可用的 clean 情感计数)不再给顶格结论——complexity.level 标为 unknown,并附 reliable: false 与 reliabilityNote 说明原因,渲染文本里也会单独一行写出这句警告;score 保留数值便于跨章对比。成人向内容 / 感官描写密集的文本,情感维度本就不适用,建议改用节奏、爽点、对话密度等维度。
7. 世界观与语用检测:文化基准自动判断(西/东/混合)+ 置信度;speechStyle 称谓/客套/仪式/语气规范;题材流派 + 网文信号。
8. 写作辅助全家桶:伏笔登记表 / 设定五张表(人物·地点·道具·时间线·世界观)/ 章节摘要 / 连贯性审计 / 批量导入 / 风格自检 / 续写辅助。
9. 全工具 UI 开关:左栏「写作助手功能」入口 + 主栏面板(DSH 0.2.0 官方席位;老宿主 < 0.2.0 自动回退旧挂载)——首页按「写作资料 / AI 行为 / 功能与工具 / 记录与维护 / 高级」分区,一个页面只讲一件事;工具行统一由 PreferenceRow 承载,开关本身就是状态(开着的开关不再整行变蓝、也不再贴「已开启」),描述只写「对你有什么用」,工具 ID 与参数不进主文案。颜色全走 --dsw-alias- 语义 token(三主题与官方同源)、控件用官方 UI primitives、DeepSeek 蓝只留给「唯一主操作」与需要注意的状态,各级页面等宽。v6.7.0 面板视觉改版:lib/client.js 渲染层重写、视觉系统 CSS 4.6 KB → 19.4 KB、52 枚线图标(每个工具行一枚专属图标),文件路径 / 数据目录占用 / state.json 统一收进页面底部的「存储位置」折叠区,首页「功能与工具」的「语义模型」一行显示引擎状态;功能面完全不变(8 个视图 / 工具集 / 后端路由在 6.7.0 内零改动),设计规格见仓库内 tools/UI-REDESIGN.md。界面底座(方案 A):源码分块 + 构建链装配(构建失败不写盘、--check 校验源与产物一致)、统一组件(PreferenceRow 单一行模式、GroupCard 分组卡、StorageDisclosure 存储位置折叠区等)、页面级 push-pop 过渡动画(零 state / 零 hook)。
10. 风格基线:文笔六维测量(句法复杂度/修饰密度/抽象度/动作密度/不确定性/留白指数)+ 按章节 μ±σ 基线带;novel_style_report 输出基线带,novel_style_check 对照新章偏差(带内 ✓ / 出带 ⚠);面板「风格基线」页的「文风六项指标」分区可自定义每维 ±% 容差(每维一行:指标名 + 「低于」「高于」两个百分比输入框;推荐值 = 原著章节波动的 1.5 倍 σ,自动取整、限 ±10%~100%;输入框留空即用推荐)——原创/续写时主题自由、写法保持在基线带内。基线范围(v6.5.5,按书选择):基线不必永远是全书——面板「风格基线」里选全书(默认,等于旧行为)/ 按部分,再用章节选择器勾出你认作标杆的章,基线就只用这几章(是「只用」,不是「不够再补」)。这是给「AI 续写越写越像 AI 自己」准备的:从第 2 章续写起,模型参考的其实是它自己写的上一章,而从全书取样的基线自己也在漂(书越长 AI 占比越高)。把标杆钉死在你亲手写的那些章上,插件就能回答「相对这几章,新章偏了多少」——它不判断你有没有漂移,那一项本就不可判定。正在检查的那一章永远排除(否则等于拿自己跟自己比);勾中的章少于 3 章时推荐容差自动放宽到至少 ±25%,所以单章也能当基线;取不到的章(已改名 / 已删除,或正文过短无法测量)会被跳过并如实计数,一章都取不到时自动回退「按全书」并在返回体里写明原因。范围按书保存(<书库根>/.novel-writer/baseline/<书名>.json),换书互不影响。六维口径未变(v7.0.0):本次只换了细节密度的词表,这六维的算法与数值都没动,新旧报告可以直接比。
11. 写作哨兵三件套:novel_continuity_check 扩展——①衔接检查(chapter 参数:时间硬跳/语义距离/人物延续/钩子承接四路检测,带原文引用)②OOC 检测(ooc 参数:角色情绪基线偏离)③大纲走偏(outline 参数:方向行 vs 正文关键词重合);报告工具支持 brief 精简模式。
12. 原创模式与创作资料:面板「原创设定」填写创作设定(世界观 / 角色设定 / 不允许的事件 / 主线目的 / 题材偏好 / 额外要求,留空=模型自定,多书独立设定库);novel_outline 维护创作资料(创作设定/人物/剧情大纲/钩子记录/创作状态卡),原创强制「设定书→大纲→钩子」链,动态批次(10→20→30 章)防剧情跳跃与角色 OOC。
13. 体验与统计:首页「写作资料」卡底部一行书库概况(「书库 3 部 · 13 章 · 5.0 万字」,点开是每本书的章数 / 字数 / 7 天未动,以及每本书旁的删除整本书…快捷入口);首页第一个分区里还有一行「删除整本书」——点进去是独立页面:选书 → 看清清理清单 → 勾选执行,页面下方就是回收站(恢复 / 彻底删除)。原来独立的「书库统计」卡已并入书库概况这一行;「记录与维护」里有体验演示(内置示例不落盘跑六项文风指标,不保存任何内容)、报告历史(已保存的句式分析与风格报告,按书分组,每份都可单独删除——删除走回收站,仍可恢复)与诊断报告(统计窗口 + 一键复制诊断包,用于贴进 issue);错误提示带解决步骤;工具说明压缩省 token。v7.0.0 修尺子:细节密度词表去噪、动作链示例改为完整句等距抽样 3 条、面板「诊断报告」页新增「度量口径」区可一键重建旧口径缓存(只删派生缓存,不动你选的基线范围)。
14. 非净化模式(rawWriting,默认关闭,6.9.0 重做):一个写作约束开关,不是模型能力开关;开启需双重确认 + 承诺输入。开启后分三层工作——动笔前:开写包多出【非净化模式·目标带】(原著「把场面绕过去」的写法密度,处/千字,启发式;本章目标带;一条硬约束:留在场景里 / 细节密度对齐原著 / 不用省略号或跳场带过 / 不用总结代称 / 不插评论与免责);动笔时:模型若在开头就自我刹车(「这一段我不展开」这类),llm/stream 那一层会丢弃这次输出并只重试一次(重试只在系统消息尾部追加写作指令,不改你的输入);写完后:novel_style_check 给出「本章 X 处/千字 vs 原著 Y 处/千字」与一句结论(clean / new / similar / softer / much-softer / more-explicit)——干的是「有没有被绕过去」的核对,不是评分。已知边界:直白度量是启发式(只数明确的淡化写法);重试只一次且只认三类自我刹车特征;这一层依赖宿主 llm/stream,服务缺失时安静跳过并在日志留痕;效果取决于模型本身,插件不保证产出。
提供的工具(21 个)
每个工具的参数、返回结构与示例,见在线工具手册。
| 工具 | 说明 |
| novel_books | 列出章节库全部作品 |
| novel_chapters | 列出某作品章节清单 |
| novel_read | 阅读某章正文(分段) |
| novel_keywords | 关键词:二字组/三字组/疑似人名 |
| novel_new_chapter | 创建新章节文件 |
| novel_import | 原稿件批量导入/分类 |
| novel_sentence_analysis | 句式模式分析(九类/情感净化/量化/曲线/指纹) |
| novel_sentence_config | 查看/修改工具与功能开关(含提示词场景) |
| novel_style_check | 风格自检(规则+语义双维度) |
| novel_style_report | 风格画像报告(6 维测量 + AI 判断分离) |
| novel_plot | 伏笔/剧情线登记表;action: "graph" 结构视图(伏笔埋设跨度 / 人物连续缺席 / 剧情线空档 / 时间线顺序 / 大纲对照) |
| novel_settings | 设定管理(人物/地点/道具/时间线/世界观) |
| novel_summary | 章节摘要(长书续写辅助) |
| novel_continuity_check | 连贯性审计 + 衔接/OOC/大纲走偏哨兵 |
| novel_semantic_search | 语义检索(本地 embedding,0 token) |
| novel_outline | 创作资料管理(创作设定/人物/大纲/钩子/状态卡) |
| novel_chapter_brief | 开写包——动笔前一次调用取齐材料(上一章承接口/本章方向/相关人物/待回收伏笔/用语规范/风格基线/锚段与骨架/禁用清单/开写清单) |
| novel_fix_plan | 改稿台——把风格诊断变成按优先级排好的待办(带行号定位与锚段),改完可复测;只给方向不生成正文 |
| novel_lexicon | 必用词表——专有词/作者惯用词的书级+全局两层登记、批量导入、开写包硬清单注入、成稿用词审计 |
| novel_diagnose | 诊断报告(v6.6.0)——读本地事件日志 + 五处体检(书库根 / 书库内容 / 数据目录损坏 / 语义引擎 / 开关快照),聚合近期调用/降级/失败、「工具 × 书名」反复出问题的组合、高频失败类型与可执行建议;report / events / clear 三态。报告只含元数据(工具名/书名/章号/状态/耗时/计数),自由文本在分享层已被整体剔除、错误一律归类成固定 code,可安全贴进 issue(报 bug 请用它或面板「复制诊断包」,不要直接附本机日志文件) |
| novel_delete_book | 按书删除/清理(v6.8.0)——一本书的资料散在 12 处(正文 / 创作资料 / 数据目录 9 个按书子目录 / 旧位置伏笔表 / 设定库条目),这里一处预览、一处清理。按「前缀 + 分隔符边界」匹配文件名(删《孤舟》不会碰到《孤舟子》);默认 preview 只算不写盘,apply 移入 <数据目录>/trash/<书>-<时间戳>/ 并写清单,restore 原样移回(同名文件跳过、绝不覆盖),purge 才彻底删除。keys 可只清其中几处(例如只删报告历史与缓存、正文留着)。全局词表、插件设置与其他书的文件一律不动 |
MCP 服务器(非 DSH 用户也能用)
包里自带一个 stdio MCP 服务器(mcp/server.mjs),把 21 个工具原样暴露给任何 MCP 客户端,例如 Claude Desktop、Cursor。它是跑在你自己电脑上的本地进程,不需要服务器、不需要联网、不需要常驻。
npx -y -p dsh-novel-writer dsh-novel-writer-mcp --root /你的小说库路径
客户端配置示例(claude_desktop_config.json / Cursor mcp.json):
{
"mcpServers": {
"dsh-novel-writer": {
"command": "npx",
"args": ["-y", "-p", "dsh-novel-writer", "dsh-novel-writer-mcp", "--root", "/你的小说库路径"]
}
}
}
书库根目录优先级:--root > 环境变量 DSH_NOVEL_WRITER_ROOT > 当前工作目录。
细节见 mcp/README.md。
配置
- id: novel-writer
config:
root: 'D:/我的小说库' allowLanState: false # true=局域网访问 GUI 时也允许保存开关
数据目录
<书库根>/.novel-writer/:plots(伏笔)/ settings(设定)/ summaries(摘要)/ analysis(分析报告)/ audits(连贯性审计 + 改稿清单 fix-plan-.json)/ embedding(语义索引)/ style-reports(风格画像)/ baseline(风格基线)/ lexicon(必用词表,_global.json 为全局层)/ trash(按书删除的回收站,<书>-<时间戳>/ + manifest.json)。
回收站不自动清理:trash/ 里的内容仍占磁盘(这正是"还能恢复"的代价),所以删书后面板的「数据目录」体积会变大而不是变小;确定不要了就用 novel_delete_book action="purge" 或手动删掉对应子目录。
事件日志不在这里:它写在状态文件旁——~/.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 的报告或面板「复制诊断包」——那条路走分享层,自由文本整体剔除。
依赖、权限与失败边界
运行时依赖(npm install 自动安装,均为公开包):
- onnxruntime-web ^1.24.3 —— 本地 ONNX 推理(WASM 后端),用于语义检索与语义风格距离;
- @huggingface/tokenizers ^0.1.0 —— 中文分词(WASM);
- peerDependency:react ^18.2.0(浏览器端复用 DSH Web GUI 自带的 React,不额外打包)。
本地模型:lib/models/ 随包分发 bge-small-zh-v1.5 量化模型(约 24MB,ONNX)与分词器(tokenizer.json.gz,加载时解压)。全部推理在本机 CPU 完成,不上传任何文本。
权限与外部服务:
- 文件系统:读写用户指定的书库根目录 novels/ 与其数据目录 <root>/.novel-writer/,以及插件自身的开关文件 ~/.dsh/dsh-novel-writer/state.json。例外一处:novel_import 的 src 按设计可以指向任意目录(用于把别处的旧稿导入书库),mode:"apply" + move:true 会删除源文件——删改范围由调用方决定,请只在明确知道源目录内容时使用。例外仅此一处:MCP 服务器默认把这个 src 也限制在书库根内(见下方 MCP 一节)。
- 内置技能:通过 ctx.skills 注册自带 novel-writing 技能,只读取包内 skills/novel-writing/SKILL.md,不写入任何技能目录、不需要改宿主配置;宿主没有 skills 服务时静默跳过。
- 本地 HTTP:在 DSH Web GUI 内注册 8 条路由(state / reveal / reports / demo / update-check / lexicon / baseline / diagnose),仅回环地址可访问;allowLanState 默认关闭,局域网访问默认拒绝。日志与诊断都在本机:事件日志写在 ~/.dsh/dsh-novel-writer/logs/events.jsonl(512 KB 轮转、留 3 份),以元数据为主(时间 / 工具名 / 书名 / 章号 / 状态 / 耗时 / 计数 / 路径 / 错误 code),但自由文本字段(错误消息等)经收敛后至多保留一句(≤160 字)——所以本机日志里可能出现被错误消息回显的输入片段,该文件不出本机; novel_diagnose 与面板「复制诊断包」走分享层,自由文本整体剔除后才输出(任何离开本机的内容都不含自由文本);分享层是默认行为 (readLogEvents 只有显式 forShare:false 才返回带自由文本的本地层,其余一律脱敏——隐私边界的默认值偏向「少给」)。
- MCP 服务器(mcp/server.mjs):工具参数里的 root 必须落在启动时 --root 指定的书库根之内,越界会被拒绝并回退;novel_import 的 src 默认也限根内,确需导入外部目录时用 --allow-external-src 显式放开。单行报文上限 4 MiB,超限整行丢弃;stderr 默认只记一行错误摘要,不回显正文与堆栈(需要完整堆栈用 DEBUG=1);长任务可用 notifications/cancelled 取消。详见 mcp/README.md 第 0 节。
- 外部网络:唯一外呼是 GitHub Releases API(api.github.com)检查新版本——3 秒超时、24 小时缓存、失败静默降级;请求不含任何书籍内容。
- 子进程:无。唯一例外是打开系统文件管理器(Windows explorer / macOS open / Linux xdg-open),以数组参数直调、不经 shell。
- 生命周期脚本:无(无 preinstall / postinstall / prepare)。
失败边界:
- 语义引擎不可用(模型缺失或 WASM 初始化失败)时自动回退纯规则模式,其余功能不受影响;
- 分析结果落盘失败不阻塞工具返回;缓存损坏按「无缓存」处理并重建;
- 「读不到」不等于「没有」:只有文件真的不存在(ENOENT)才说「尚未初始化 / 未登记 / 尚未保存 / 无创作资料」;文件存在却读不出来(损坏 / 无权限 / 编码识别不了)时只说事实并给出路径与原因,并说明不必去 init——同时记一条事件日志,可用 novel_diagnose 查真因;
- 已知残留(下版处理,如实记下):把某章文件换成同名目录时,scanChapters 里那句 if (!entry.isFile()) continue 会静默跳过它——该章从章表消失(novel_chapters 少一章)、novel_diagnose 的「读不出来的文件」计数为 0、novel_chapter_brief 的 degraded 里也没有「读取章节失败」。也就是这一条路径上「读不到」仍被讲成「没有」;遇到「章数莫名少了一章」时,先确认该书目录下是不是有一个叫章节名的目录;
- 插件加载失败不影响 DSH 主进程:工具注册与提示词注入相互独立。
兼容范围:Node.js >= 22.3(engines.node);DSH >= 0.1.1-rc.2(dsh.engines.dsh)。
许可证
MIT
🔗 官网 · 技术文档 · GitHub · npm · MCP 服务器手册预览分享卡片长什么样 ↓
给中文网文作者的本地写作工作台(20 个工具):动笔前取齐上一章承接口、大纲、人物卡、待回收伏笔、用语规范;写完后按六维文笔基线对照原著波动带自检,排出带原句行号的改稿待办;跨章查伏笔埋设跨度、人物连续缺席与大纲偏离…
相关插件 · 工具 / 效率
这是你的仓库?认领条目