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

novel_outline

原创小说创作资料的维护工具。在 novels/创作资料/<书名>/ 下管理六个 Markdown 文件:创作设定、主要人物设定、次要人物设定、剧情大纲、钩子记录、创作状态卡。init 铺模板,read 读回全文,其余五个动作分别做权威设定覆盖、人物登记、逐章方向行追加、逐章钩子回填与状态卡刷新。落盘物是纯 Markdown,可被作者直接手改;工具用标题与「- 章号 」行首标记来识别结构,不依赖额外索引文件。

参数

参数类型必填说明
bookstring书名(novels/创作资料 下的子目录名)。经 sanitizeSegment 过滤。
actionstring枚举七值,见下表。本工具是四个页面中唯一把 action 列入 required(实现内另有 args?.action ?? "read" 兜底)。
filestringread 时指定要读的文件:bible / characters-main / characters-minor / outline / hooks / status;省略默认 status
contentstringbible(创作设定全文)与 status(状态卡全文)时必需
rolestring枚举 main / minorcharacter 时使用;非 main 的一切取值(含省略)按 minor 处理。
namestringcharacter必需:人物名。
descriptionstringcharacter 时:人物简介(目标 / 动机 / 弱点 / 说话方式)。
numbernumberchapter / hook必需:章节号。
titlestringchapter 时:本章标题 / 方向,单行化后写入。
bodystringhook必需:本章结尾钩子(状态 / 悬念 / 时间场景)。
rootstring章节库根目录。

action 取值

取值语义前置要求写入对象
init初始化创作资料(缺文件建模板,已存在的做段级同步)六个文件
read读回指定文件全文file 必须是六个键之一且文件已存在不写盘
bible写入创作设定content创作设定.md(整文件覆盖)
character登记人物(行级追加,同名不重复登记)name主要人物设定.md次要人物设定.md
chapter补一行大纲方向行number 可解析为整数剧情大纲.md
hook回填 / 覆盖某章结尾钩子number、非空 body钩子记录.md
status刷新创作状态卡content创作状态卡.md(整文件覆盖)

输出结构

输出 schema 是四个工具中唯一使用 additionalProperties: true未声明任何 required 字段的:

字段类型出现条件
bookstring恒返
actionstring恒返(原样回填 args.action,不做枚举归一)
filestringread:实际读取的键名(bible / hooks / …),不是文件路径
contentstringread:文件全文,可能追加未回填钩子警告
messagestring其余动作:结果文案

渲染层只打印其中一段:action === "read"content 非空时输出 content,否则输出 message

数据模型与落盘

<root>/novels/创作资料/<书名>/
├── 创作设定.md          # 世界观规则 / 主线冲突 / 分卷目的 / 禁忌
├── 主要人物设定.md      # 目标→动机→弱点→说话方式→关系
├── 次要人物设定.md      # 谁 / 作用 / 特征;不再出场标注
├── 剧情大纲.md          # 每章方向行
├── 钩子记录.md          # 每章结尾钩子
└── 创作状态卡.md        # 进度 / 上一章结尾 / 时间线 / 活跃角色 / 下章方向 / 未回填钩子

键名与文件名的固定映射(CREATION_FILES)是工具内唯一的路径来源,目录名常量 CREATION_DIR = "创作资料",与作品目录同级挂在 novels/ 下:

file / 键名文件名
bible创作设定.md
characters-main主要人物设定.md
characters-minor次要人物设定.md
outline剧情大纲.md
hooks钩子记录.md
status创作状态卡.md

行级数据的两条约定格式:大纲方向行为 - <章号> <标题>,钩子记录行为 - <章号> <钩子文本>。未回填钩子的判定只认这两种行首标记。

init:模板铺设与段级同步

  • 目录以 recursive 方式创建;六个文件不存在才写模板,已存在的一律不覆盖正文。
  • bible 已存在时,只对「【用户原创设定】」段做 upsert:用侧边栏填写的原创设定(世界观 / 角色设定 / 不允许的事件 / 主线目的 / 题材偏好 / 额外要求)重建该段;侧边栏为空则删除该段。作者手写的其余部分原样保留。
  • characters-main 无论新建还是已存在,都会对「【用户角色设定】」段做同样的 upsert。
  • 返回文案为「创作资料已初始化(6 个文件)」,计数是模板条目数,不是实际新建的文件数。

段级 upsert 的边界

upsertCreationSection(text, section, marker) 用固定正则在整篇 Markdown 里定位一个「【…】」段:

(?:\\r?\\n){0,2}MARKER[\\s\\S]*?(?=\\r?\\n\\s*#|\\r?\\n\\s*【|\\s*$) MARKER = 段标记(如【用户原创设定】),正则元字符先转义 段尾 = 下一个 # 标题 / 下一个【…】段 / 文件尾 section === "" 且有该段 → 整段删除后压缩连续空行,并在末尾补一个换行 有该段且 section 非空 → 函数式替换,写入 "\\n\\n" + section.trimEnd() 无该段 → 追加 "\\n\\n" + section
v4.3.0 修正:段尾不能认空行。旧前瞻把「空行」也算作段尾,段内一旦出现空行就只覆盖到第一个空行——删除时残留(- 额外:B 留在文件里)、替换时新旧两段并存(标记出现两次)。现在只认真正的段尾标记,前缀容忍空行与 CRLF(记事本编辑过的文件),标记本身做转义。替换一律使用函数式回调,避免用户文本里的 $& / $1 被当作替换模式展开。

read:白名单、初始化检查与钩子提醒

  • file 省略时取 status。键名用 Object.prototype.hasOwnProperty 判定:constructor / __proto__ 这类原型链上的名字不再被当成合法文件,而是抛「未知文件:…(可用 bible/characters-main/characters-minor/outline/hooks/status)」。
  • 文件读取失败(目录未建或文件缺失)时抛「创作资料尚未初始化,请先 novel_outline init(或在创作文件夹自行创建)」。
  • statusoutline 时额外做未回填钩子检查:从大纲提取全部 ^[-*] (\d+) 行首章号(读 status 时会另外读大纲文件,因为状态卡只有快照、不含章节行),从钩子记录提取同样的章号集合,取「在大纲、不在钩子」的差集。
  • 差集再与磁盘上确实存在的章节号求交——大纲按设计会预先列出尚未动笔的章节方向,若不求交就会把没写的章也算成「写完却没回填」。章节扫描失败时退回不求交的旧口径(宁可多提醒)。
  • 差集非空时在 content 末尾追加「⚠ 未回填钩子章节:N、M——写完的章节必须回填钩子(novel_outline hook),下一章开头从钩子接续」。返回的 content 因此不等于磁盘原文

character / chapter / hook 的行级写法

动作写入行去重判定重复时的行为
character- 名字:简介行首锚定正则 ^[-*] 名字(:|$)(多行模式,名字做正则转义)不写盘,返回「人物“X”已存在(未重复登记)」
chapter- 章号 标题全文子串 - 章号␠不写盘,返回「第 N 章已在纲(未重复追加)」
hook- 章号 钩子文本行级正则 ^- 章号 [^\n]*(?:\n|$)(多行模式)整行替换为新钩子

三者都会在文件缺失时按需重建(带上 # 主要人物设定 / # 剧情大纲 / # 钩子记录 一级标题与空行);文件存在但缺少对应标题时不报错,只在文件头补一行标题再追加内容。文本规范化:characterdescription 把换行折叠成「;」,chaptertitlehookbody 折叠成空格(单行化,防止换行伪造出一条新条目);hook 额外剥离 body 行首可能被误传的「- 章号 」前缀。而 bible / statuscontent 不做任何规范化,原样整文件写入。

约束与边界

  • bookaction 都必填。action 不在七个枚举值内时抛出「未知 action:xxx」——这是本工具与 novel_plot / novel_settings / novel_summary 的显著差异:三者对非法 action 静默退化为 list,本工具直接失败。
  • action 不做归一:返回的 action 是传入值原文,调用方不能靠它判断「实际执行了什么」——不存在的动作根本不会返回。
  • read 需要文件已存在:目录未初始化时抛错并提示先 init,不会返回空字符串。
  • bible / status 是整文件覆盖:不做增量合并,调用方必须提交全文。分两次只传局部会把前一次的正文抹掉。两者都要求 content非空字符串(纯空白视同未传)。
  • statusbible 没有段级保护:与 init 的「只动【用户原创设定】段」不同,直接调 bible 会覆盖作者手写的全部内容。
  • 人物与章号只能增改、不能删:本工具没有删除人物的动作;chapter 不会删除已有方向行,hook 只能整行覆盖已有钩子。清理只能由作者手改 Markdown(手改是受支持的用法,重号判重与标题补全都会容忍)。
  • numberparseInt 解析:schema 声明为 number,实现按 parseInt(args.number, 10) 处理,因此 "3章" 会被截取成 3;只有解析结果为 NaN 才抛「chapter 需要 number」/「hook 需要 number」。
  • hookbody 不能为空:单行化后为空的 body 抛「hook 需要 body(本章结尾状态/悬念/时间场景)」。chaptertitle 可以为空,此时写入的是「- N 」这样一条只有章号的方向行(行尾保留一个空格)。
  • role 静默退化role:"supporting" 不会报错,会写入次要人物设定。
  • 人名判重是行首锚定的 不会因文件里已有 阿澈 而被判重复(旧实现按子串判重,会把「阿」误判为已存在)。反过来,同一人物换个名字登记会产生两条记录,本工具没有别名合并。
  • chapter 的判重是全文子串,粒度比 hook 的行首正则更粗:正文或注释里出现「- N 」这样的片段也会让本次追加被跳过。需要精确控制时应改成 hook 式的行首锚定路径或手改文件。
  • 未回填钩子的判定依赖磁盘章节号:作品目录下没有任何章节文件时,existing 为空集合,提醒列表随之为空(不会提示「大纲里的章都没回填」)。
  • 没有事务与原子写:与其余三个工具不同,本工具的写入是「读全文 → 拼接 / 替换 → writeFile 整文件覆盖」,未使用 withFileTx,也未使用临时文件 + rename 的原子替换。同一文件的并发写可能互相覆盖,且进程在写入中途终止会留下半截文件。批量登记时建议串行调用。
  • 跨版本口径:v4.3.0 修正段级 upsert 的段尾判定(旧版遇段内空行会截断,导致删除残留、替换双段并存)、修正未回填钩子口径(只报磁盘上存在的章节,旧版把大纲里预排的下一批章节也算成未回填)、readfile 改为 hasOwnProperty 判定并删除了从不产出的 files 字段、补上真实返回的 file 字段;v3.1.0 起 description / title / body 做单行化。旧版写下的文件在段结构上可能与新版的 upsert 结果不同,重新 init 会就地重整对应段。
  • MCP 形态下的 root 限制:参数 root 必须落在服务器启动参数 --root 的书库根之内,越界会被静默回退,同时记 stderr。
  • 参数表禁止额外字段parameters.additionalProperties = false

相关工具

  • novel_continuity_check —— 大纲对照模式直接读本工具的 剧情大纲.md,取 ^- \d+ 方向行与正文开头做二元组重合率比对。
  • novel_settings —— 五张设定表(人物 / 地点 / 道具 / 时间线 / 世界观用语规范),与本工具的 Markdown 人物设定是两套并行资料。
  • novel_plot —— 伏笔登记表,与「钩子记录」分别追踪长线伏笔与逐章结尾钩子。
  • novel_summary —— 逐章摘要,与「创作状态卡」的进度快照互补。

源码位置

  • lib/core.js —— createOutlineTool(约 1669–1860 行):七个动作的完整实现。
  • lib/core.js —— CREATION_DIR / CREATION_FILES / CREATION_TEMPLATE_TITLES(约 1605–1610 行)、creationDir(约 1611 行)。
  • lib/core.js —— upsertCreationSection / upsertCreationUserSection(约 1634–1657 行)、buildCreationUserSection / buildCreationCharacterSection(约 1614–1668 行)。
  • lib/index.js —— registerNovelOutline(约 2876–2878 行):仅把 createOutlineTool(config) 注册给宿主。
  • mcp/server.mjs —— buildArgs / isInsideRoot(约 259–289 行)。