← dsh-novel-writer 技术文档 / novel_new_chapter

novel_new_chapter

新建章节。为指定作品创建一个章节文件,章号可显式指定或自动取下一个,可同时写入 Markdown 一级标题与初始正文。同号章节一律拒绝创建,绝不覆盖既有稿件;创建成功后返回体自带全书六维风格基线 μ 摘要与「风格锚包」(原著代表性段落 + 句式骨架),使调用方模型在动笔前即可拿到测量数据与用于校准语感的样例,无需先调用风格报告工具。

参数

参数类型必填说明
bookstring书名,即 novels/ 下的子目录名。经 sanitizeSegment 清洗:剥离 \ / : * ? " < > |、首尾点与空白,Windows 保留名(CON / NUL 等)加下划线前缀;清洗后为空则报错。目录不存在时自动创建。
chapterinteger显式章号,须为 1–99999 的整数。省略时取现有最大可解析章号 + 1。
titlestring章节标题,写为正文首行 # 标题,并(v5.0.0 起)一并进文件名第NN章 标题.md。空串或纯空白视为未提供。
contentstring初始正文。空串或纯空白视为未提供。
rootstring章节库根目录。缺省回退顺序:args.rootconfig.root → 会话工作目录。MCP 形态下必须落在启动参数 --root 之内。

参数对象声明 additionalProperties: false,多传的键会被宿主拒绝。

输出结构

契约字段(output.schema.required):book / number / file / path / chars 五项必返;其余字段按条件出现。返回前经 cleanOutput 处理,值为 undefined 的键会被整体剔除。

字段类型内容
bookstring必返。清洗后的书名
numberinteger必返。本次实际使用的章号
filestring必返。文件名:无 title 时为 第NN章.md(章号左补零到至少 2 位);给了 title 时为 第NN章 标题.md(v5.0.0 修正:标题经 sanitizeSegment 剥掉 \ / : * ? " < > | 等非法字符并截断到 24 字;标题全是非法字符则退回不带标题的文件名,不因此拒绝创建
pathstring必返。落盘绝对路径
charsinteger必返。写入文本的字符数(合成文本整体长度,含标题行与结尾换行)
baselinestring全书六维基线 μ 摘要,形如「标签 值 / 标签 值 …」,无法计算时为缺失
reminderstring基线怎么用」提示(v5.1.0 改口径:此前这段标题里带「强制」二字,现改为「基线怎么用」):① 锚段读一遍校准语感不要照 skeletons 造句)② 写完建议跑一次 novel_style_check 自检,偏离明显时才逐句修正。当前实现下恒有值
anchorsarray风格锚·原著段落,元素 { label, text }最多 5 条
skeletonsarray句式骨架,元素 { type, text }最多 4 条

行为与实现

本工具不涉及统计模型,全部为确定性文件操作与规则抽取。执行顺序:校验 → 建目录 → 扫描现有章节 → 定章号 → 查重 → 合成文本 → 独占写入 → 计算基线 → 构建锚包 → 记录书库根。

1 · 章号解析与文件名

number = chapter ?? ( max( 现有章节的可解析章号 ) + 1 ) ← 无任何可解析章号时 max 取 0,即自动落到第 1 章 fileName = "第" + String(number).padStart(2, "0") + "章.md"

现有章节由 scanChapters 收集:只认书目录顶层的 .md / .markdown / .txt(扩展名小写比较),子目录中的文件不参与,排序为「章号升序(不可解析者排最后)+ 文件名 localeCompare」。章号解析入口先做全角宽度归一(第01章第01章),因此下列写法都解析为同一章号:第1章.md第01章.md第一章.md书名-01-标题.md书名 01.md。中文数字支持到「千」位(如 第一千零一章 解析为 1001)。

2 · 防覆盖与独占写入

mkdir( bookDir, { recursive: true } ) ← 目录缺失不抛错,空目录按零章处理 chapters = scanChapters( bookDir ) sameNo = chapters.find( c => (c.number ?? -1) === number ) if (sameNo) → 抛错:第 N 章已存在(旧文件名)——不会覆盖旧稿 parts = [] if (title 已提供) parts.push("# " + title, "") if (content 已提供) parts.push(content, "") text = parts.join("\n") writeFile( filePath, text, { encoding: "utf8", flag: "wx" } ) ← O_EXCL 独占创建 EEXIST → 抛错:第 N 章刚被并发创建,本次写入已放弃;请重试或指定其他章号

查重(按解析出的章号)与写入(按文件名是否存在)是两套口径,两者之间存在竞态窗口;wx 标志把该窗口收敛为一次可上报的失败,而不是静默互相覆盖。已知取舍:建目录发生在查重之前,因此同号创建失败时书目录仍可能已被创建。

3 · 文本合成与字符数口径

仅 title → "# 标题" + "\n" 仅 content → 正文 + "\n" title 与 content → "# 标题" + "\n" + "\n" + 正文 + "\n" 两者皆无 → ""(空文件,chars = 0) chars = text.length ← UTF-16 码元数,不是纯正文长度

标题行与正文之间由数组 join 产生的空行是格式的一部分,会一并计入 chars。该字段用于核对「是否真的写入了内容」,不代表正文字数。

4 · 风格基线 μ 摘要

bTexts = 重扫目录后的全部章节(含刚创建的新章,其文本直接复用内存中的 text) perCh = metricChaptersCached( root, book, bTexts ) ← 按内容哈希缓存六维测量 excluding = perCh.filter( pc => pc.file !== 新章文件名 ) if ( excluding.length > 0 ) baseline = METRIC_ORDER.map( k => 标签[k] + " " + ( 有 mu ? mu : "-" ) ).join(" / ")

基线测量集合包含新章,以与风格报告、风格自检的「全章集合」保持同一缓存键(否则三者交替调用会全部缓存 miss);但摘要本身排除新章,因为新章不能作为自己的基线。无其它章节时不输出 μ=0 的假基线。novel_style_report 基线计算中「正文不足 40 字的章节不参与统计」的规则由测量层统一执行。整段被 try/catch 包裹:失败静默,不影响章节创建。

5 · 风格锚包

语料 = 创建前的既有章节文本拼接(新章不作为「原著样例」) 段落 = 按 /\n+/ 切分、trim、保留长度 ≥ 40 字的段;抽样方式为全书等距 (首尾保留、均匀取 n 条;n=1 取中位段) [对话] 含引号且 ≤ 200 字 → 2 条 [心理] 含 想|觉得|心|怕|慌|记忆 且无引号且 ≤ 200 字 → 1 条 [描写] 无引号且 80–200 字 → 2 条 以上全空 → 退化为 [原文] 3 条 每条截断至 200 字;合计 slice(0, 5) 骨架 = 按 /[。!?…]+/ 切句、去空白、长度 20–45 字、去重后等距取 4 条 类型判定:含引号 → 对话;以 ?/? 结尾 → 疑问;含 想|觉得|仿佛|好像|似乎 → 心理;否则 陈述 骨架全空 → 退化为 4 条 ≥ 15 字的陈述型段落(截 60 字)

锚包是「校准语感」的载体,不是造句模板:anchors 读一遍对上味道即可,skeletons 只在写不出该类型句子时参考,不要照它造句、不要套句式模板;数字基线只用于事后校验。整段同样被 try/catch 包裹,失败时返回空锚包。

6 · 渲染文本

<path>…</path> <type>novel-chapter-created</type> <content> 已创建 第NN章(N 字) [可选] 【风格基线 μ】… [可选] 【风格锚·原著段落】读一遍校准语感(数字基线只做事后校验): [标签] 段落… [可选] 【句式骨架】写不出该类型句子时才参考,不要照它造句: [类型] 句子… reminder ← 【基线怎么用】块:① 锚段读一遍校准语感,不要照 skeletons 造句 ② 写完建议自检,偏离明显才逐句修正 </content>

约束与边界

  • 同号即失败,不覆盖第1章.md / 第01章.md / 第一章.md 都按章号 1 拦截。需要改写既有章节时应另选章号或先由调用方删除旧文件——本工具不提供任何覆盖或更新通道。
  • chapter 的取值范围是硬断言:非整数(浮点、数字字符串)、0、负数、大于 99999 一律被拒绝并报错,而不是截断到边界。
  • 并发只报错不重试:独占创建失败时返回「刚被并发创建」,需调用方自行重试或改用其它章号。
  • 建目录是前置副作用:即使后续因同号失败,书目录也已被创建。
  • 基线/锚包均为可选增强:任一环节抛错都被吞掉,返回体中相应字段缺失或为空数组。调用方不应假定 baseline 恒有值——单章作品、基线计算失败、锚包抽取失败都会导致缺失。
  • 只统计顶层文件scanChapters 不递归,放在子目录里的章节文件对本书「不可见」,既不计入章号也不参与基线。
  • 自动章号依赖可解析章号:目录内若存在无法解析章号的文件(命名完全不含数字或中文数字),它们被排到末尾且不参与 max;此时「下一个章号」由可解析的最大值决定,可能与文件名视觉顺序不一致。
  • 章号解析的已知缺口:中文数字解析器识别 / / ,但不识别「万」——第一万章.md 解析不出章号,会被当作不可解析文件排到列表末尾,既不参与自动章号计算,也不阻止同号冲突检测(该文件不占用任何章号)。四位以内的阿拉伯章号不受此影响。
  • root 与 MCP 边界:MCP 形态下服务器为未显式提供 root 的调用注入 --root 书库根;显式传入越界路径会被静默回退书库根。
  • 工具开关execute 首行执行 assertToolEnabled,若该工具在「写作助手功能」UI 中被关闭,调用直接报错(可用 novel_sentence_config 重新开启)。
  • 写盘编码固定为 UTF-8,不写 BOM,换行为 \n;不生成备份文件与临时文件。

相关工具

  • novel_chapters —— 创建前查看现有章号,避免显式 chapter 撞号。
  • novel_read —— 按章号或标题读回刚创建的正文。
  • novel_style_check —— reminder 建议的写后自检,用同一套六维基线与目标章比对;偏离明显时才据其修正。
  • novel_import —— 已有整批原稿件时的入库入口,替代逐章创建。

源码位置

  • lib/index.js —— 工具注册与 execute:参数 schema、输出契约、基线摘要、提醒文案(registerNovelNewChapter,约 2879 行)
  • lib/core.js —— sanitizeSegment 路径段清洗、resolveRoot 根目录回退、bookDir / novelsDir 路径拼接、scanChapters 章节扫描、parseChapterNumber 章号解析、cleanOutput 输出清理
  • lib/core.js —— buildStyleAnchorPackage 风格锚包抽样与分类(约 2071 行)、metricChaptersCached 六维测量缓存(约 1864 行)
  • lib/style-metrics.js —— computeBaselineFromPerChapter / METRIC_ORDER / METRIC_LABELS,基线 μ 的计算与维度顺序