novel_outline
原创小说创作资料的维护工具。在 novels/创作资料/<书名>/ 下管理六个 Markdown 文件:创作设定、主要人物设定、次要人物设定、剧情大纲、钩子记录、创作状态卡。init 铺模板,read 读回全文,其余五个动作分别做权威设定覆盖、人物登记、逐章方向行追加、逐章钩子回填与状态卡刷新。落盘物是纯 Markdown,可被作者直接手改;工具用标题与「- 章号 」行首标记来识别结构,不依赖额外索引文件。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
book | string | 是 | 书名(novels/创作资料 下的子目录名)。经 sanitizeSegment 过滤。 |
action | string | 是 | 枚举七值,见下表。本工具是四个页面中唯一把 action 列入 required 的(实现内另有 args?.action ?? "read" 兜底)。 |
file | string | 否 | read 时指定要读的文件:bible / characters-main / characters-minor / outline / hooks / status;省略默认 status。 |
content | string | 否 | bible(创作设定全文)与 status(状态卡全文)时必需。 |
role | string | 否 | 枚举 main / minor。character 时使用;非 main 的一切取值(含省略)按 minor 处理。 |
name | string | 否 | character 时必需:人物名。 |
description | string | 否 | character 时:人物简介(目标 / 动机 / 弱点 / 说话方式)。 |
number | number | 否 | chapter / hook 时必需:章节号。 |
title | string | 否 | chapter 时:本章标题 / 方向,单行化后写入。 |
body | string | 否 | hook 时必需:本章结尾钩子(状态 / 悬念 / 时间场景)。 |
root | string | 否 | 章节库根目录。 |
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 字段的:
| 字段 | 类型 | 出现条件 |
|---|---|---|
book | string | 恒返 |
action | string | 恒返(原样回填 args.action,不做枚举归一) |
file | string | 仅 read:实际读取的键名(bible / hooks / …),不是文件路径 |
content | string | 仅 read:文件全文,可能追加未回填钩子警告 |
message | string | 其余动作:结果文案 |
渲染层只打印其中一段: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 里定位一个「【…】」段:
- 额外:B 留在文件里)、替换时新旧两段并存(标记出现两次)。现在只认真正的段尾标记,前缀容忍空行与 CRLF(记事本编辑过的文件),标记本身做转义。替换一律使用函数式回调,避免用户文本里的 $& / $1 被当作替换模式展开。
read:白名单、初始化检查与钩子提醒
file省略时取status。键名用Object.prototype.hasOwnProperty判定:constructor/__proto__这类原型链上的名字不再被当成合法文件,而是抛「未知文件:…(可用 bible/characters-main/characters-minor/outline/hooks/status)」。- 文件读取失败(目录未建或文件缺失)时抛「创作资料尚未初始化,请先 novel_outline init(或在创作文件夹自行创建)」。
- 读
status或outline时额外做未回填钩子检查:从大纲提取全部^[-*] (\d+)行首章号(读status时会另外读大纲文件,因为状态卡只有快照、不含章节行),从钩子记录提取同样的章号集合,取「在大纲、不在钩子」的差集。 - 差集再与磁盘上确实存在的章节号求交——大纲按设计会预先列出尚未动笔的章节方向,若不求交就会把没写的章也算成「写完却没回填」。章节扫描失败时退回不求交的旧口径(宁可多提醒)。
- 差集非空时在
content末尾追加「⚠ 未回填钩子章节:N、M——写完的章节必须回填钩子(novel_outline hook),下一章开头从钩子接续」。返回的content因此不等于磁盘原文。
character / chapter / hook 的行级写法
| 动作 | 写入行 | 去重判定 | 重复时的行为 |
|---|---|---|---|
character | - 名字:简介 | 行首锚定正则 ^[-*] 名字(:|$)(多行模式,名字做正则转义) | 不写盘,返回「人物“X”已存在(未重复登记)」 |
chapter | - 章号 标题 | 全文子串 - 章号␠ | 不写盘,返回「第 N 章已在纲(未重复追加)」 |
hook | - 章号 钩子文本 | 行级正则 ^- 章号 [^\n]*(?:\n|$)(多行模式) | 整行替换为新钩子 |
三者都会在文件缺失时按需重建(带上 # 主要人物设定 / # 剧情大纲 / # 钩子记录 一级标题与空行);文件存在但缺少对应标题时不报错,只在文件头补一行标题再追加内容。文本规范化:character 的 description 把换行折叠成「;」,chapter 的 title 与 hook 的 body 折叠成空格(单行化,防止换行伪造出一条新条目);hook 额外剥离 body 行首可能被误传的「- 章号 」前缀。而 bible / status 的 content 不做任何规范化,原样整文件写入。
约束与边界
book与action都必填。action 不在七个枚举值内时抛出「未知 action:xxx」——这是本工具与novel_plot/novel_settings/novel_summary的显著差异:三者对非法 action 静默退化为list,本工具直接失败。- action 不做归一:返回的
action是传入值原文,调用方不能靠它判断「实际执行了什么」——不存在的动作根本不会返回。 read需要文件已存在:目录未初始化时抛错并提示先init,不会返回空字符串。bible/status是整文件覆盖:不做增量合并,调用方必须提交全文。分两次只传局部会把前一次的正文抹掉。两者都要求content为非空字符串(纯空白视同未传)。status与bible没有段级保护:与init的「只动【用户原创设定】段」不同,直接调bible会覆盖作者手写的全部内容。- 人物与章号只能增改、不能删:本工具没有删除人物的动作;
chapter不会删除已有方向行,hook只能整行覆盖已有钩子。清理只能由作者手改 Markdown(手改是受支持的用法,重号判重与标题补全都会容忍)。 number以parseInt解析:schema 声明为 number,实现按parseInt(args.number, 10)处理,因此"3章"会被截取成 3;只有解析结果为NaN才抛「chapter 需要 number」/「hook 需要 number」。hook的body不能为空:单行化后为空的body抛「hook 需要 body(本章结尾状态/悬念/时间场景)」。chapter的title可以为空,此时写入的是「- N」这样一条只有章号的方向行(行尾保留一个空格)。role静默退化:role:"supporting"不会报错,会写入次要人物设定。- 人名判重是行首锚定的:
阿不会因文件里已有阿澈而被判重复(旧实现按子串判重,会把「阿」误判为已存在)。反过来,同一人物换个名字登记会产生两条记录,本工具没有别名合并。 chapter的判重是全文子串,粒度比hook的行首正则更粗:正文或注释里出现「- N」这样的片段也会让本次追加被跳过。需要精确控制时应改成hook式的行首锚定路径或手改文件。- 未回填钩子的判定依赖磁盘章节号:作品目录下没有任何章节文件时,
existing为空集合,提醒列表随之为空(不会提示「大纲里的章都没回填」)。 - 没有事务与原子写:与其余三个工具不同,本工具的写入是「读全文 → 拼接 / 替换 →
writeFile整文件覆盖」,未使用withFileTx,也未使用临时文件 +rename的原子替换。同一文件的并发写可能互相覆盖,且进程在写入中途终止会留下半截文件。批量登记时建议串行调用。 - 跨版本口径:v4.3.0 修正段级 upsert 的段尾判定(旧版遇段内空行会截断,导致删除残留、替换双段并存)、修正未回填钩子口径(只报磁盘上存在的章节,旧版把大纲里预排的下一批章节也算成未回填)、
read的file改为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 行)。