novel_continuity_check
连贯性审计。四种模式由参数选择且互斥:chapter 做单章衔接检查(时间跳变 / 语义距离 / 人物延续 / 钩子承接),outline 做创作资料大纲方向行与正文开头的关键词重合度对照,ooc 做登记角色的情绪基线偏离哨兵;三者都不传时对照设定表扫描全书,输出矛盾候选(同章号多文件 / 数字口径 / 人物缺场 / 别名 / 设定重复 / 用语与语用冲突)。全部判定由本地规则与本地向量引擎完成,不调用在线模型;返回值是候选而非结论,逐条需人工确认。
type: "同章号多文件" 的候选(chapters 为该组全部文件名)。这类书用章号定位的工具会直接报错(见 novel_read),所以把它放进审计里,可以在报错之前先看到是哪几章撞了。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
book | string | 是 | 书名,即 novels/ 下的子目录名。经 sanitizeSegment 过滤(剥离 \ / : * ? " < > | 与首尾点、空白),清洗后为空即报错。 |
chapter | string | 否 | 衔接检查模式。章号 / 文件名 / 标题子串,由 findChapter 解析。与 outline、ooc 互斥。 |
outline | boolean | 否 | 大纲对照模式。仅 true 计入模式判定;false 等同未传。与 chapter、ooc 互斥。 |
ooc | boolean | 否 | OOC 哨兵模式。仅 true 计入模式判定。与 chapter、outline 互斥。 |
root | string | 否 | 章节库根目录。省略时取 config.root,再退到会话工作目录。MCP 形态下必须落在启动参数 --root 之内(含根本身),越界会被静默回退该根。 |
chapter 已定义且字符串化后非空」「outline === true」「ooc === true」三项计数,计数 > 1 直接抛错,不做优先级择一。因此 chapter: "" 不算衔接模式,会落回全书扫描。
输出结构
契约必返字段(output.schema.required):book、candidates、advice。其余字段按模式出现。
| 字段 | 类型 | 内容 |
|---|---|---|
book | string | 净化后的书名 |
candidates | array | 候选列表。每项为 { type, detail, chapters? },type 与 detail 必返;chapters 为章节文件名数组 |
advice | string | 按模式与候选条数生成的处理建议 |
action | string | 哨兵模式返回 "衔接" / "大纲对照" / "OOC";无参数扫描模式不返回该字段 |
chapter | string | 衔接模式返回目标章的文件名(「第一章无上一章可比」的提前返回除外) |
reportFile | string | 仅无参数扫描模式返回审计报告路径;三种哨兵模式不落盘、不返回该字段 |
additionalProperties: false,不含 brief;渲染层也没有精简分支。输出体积由内部的候选条数与章节列表上限控制(见「约束与边界」),调用方无法用参数进一步压缩。渲染文本为 <path> / <type>novel-continuity-check</type> / <content> 包裹的清单;候选为空时输出「未发现明显矛盾候选。」。
计算原理
1 · 衔接检查(chapter)
先取上一章(scanChapters 按章号排序后的前一个文件)的末 600 字与本章开头 800 字;两段文本任一去空白后为空则直接返回空数组。四路检查依次执行,任一路的失败都只影响该路。
dePunct 指剔除以 ,。!?、 与空白后的字符集合;钩子比对是单字级集合命中率而非 2-gram,语义接续但字面弱的情况不误报。时间词表 hard 为 28 项(第二天 / 三天后 / 半年后 / 多年后 / 不久后 …),soft 为 12 项(次日 / 翌日 / 隔天 / 深夜 / 黎明 / 清晨 / 黄昏 / 午夜 / 当晚 …)。
2 · OOC 哨兵(ooc)
需要设定表已登记角色(name.length ≥ 2),否则返回空候选并提示先登记。对每个角色逐章取上下文窗口,按章求情绪均值,再与该角色的全书均值比较。
meanValence 来自 valenceStats:词表按最长优先消费区间,命中位置先过 negatedAt 否定过滤,再以 100 字窗口统计。零命中窗口不进入均值序列(避免 0 稀释),因此无情感词的章不产生效价值——该角色在该章不进样本。
3 · 大纲对照(outline)
两个提前返回:大纲文件读不到时返回 advice: "该书无创作资料…";文件存在但无方向行时返回 advice: "创作资料大纲没有方向行…"。两种都是空候选。skipped > 0 时 advice 追加「(跳过 N 章读取失败)」。本模式不读取全书正文,只按需读命中的章节开头。
4 · 全书设定表扫描(无参数)
先登记 readSettings 的五张表(characters / locations / items / timeline / worldview),再逐章读全文,按下列固定顺序生成候选。
同章号多文件(v4.3.1 起,排在全部检查之前)。对 scanChapters 的结果按解析出的章号分组,凡一组内文件数 > 1 就生成一条候选,detail 列出该组全部文件名(不套用 5 章截断——撞号分组必须完整可见)。章号解析不出(number 缺省)的文件不参与分组。
数字口径。扫描 \d+\s*(?:万亿|千万|百万|十万|[万亿千百]) 或 [零一二两三四五六七八九十]{1,2}(?:万亿|千万|百万|十万|[万亿千百]),去掉空白后要求写法以数量单位字(万亿 / 千万 / 百万 / 十万 / 万 / 亿 / 千 / 百 / 十)结尾,并剔除「十」「百」「千」「万」「亿」这类纯单位字写法。把写法解析回数值(万亿=1e12、千万=1e7、百万=1e6、十万=1e5、亿=1e8、万=1e4、千=1e3、百=1e2、十=1e1),只对数值相同而写法不同的成对条目生成候选,总数上限 20 条。
人物与别名。对每个登记人物统计「全文不含其 name 的章节」:全部章节都不含 → 「人物缺失」;部分不含且全书章节数 > 1 → 「人物缺场」;两者 chapters 都只取前 5 章。若登记了 alias 却全书无任何别名出现 → 「别名未用」(chapters 为空数组)。
设定重复。对 characters / locations / items 三张表,取 normalizeSettingList 后的 name 序列,出现次数大于 1 的名字 → 「设定重复」。
用语与语用。禁用词表来源按优先级确定:取 worldview 中最近一条 bannedWords 非空的条目;没有则取最近一条含 bannedWords 字段(含显式空数组)的条目;都没有则走默认——此时先对全书正文跑 detectCulture,判定为 eastern 时用 SPEECH_STYLE_RULES.eastern.honorBad 作禁用词表(建议词用 honorGood),判定为 modern 时禁用词表为空(整路跳过),western / mixed / unknown 时用默认欧式中世纪晚期词表。语用三路各自扫描 speechStyle.honorBad、speechStyle.ritualBadPatterns(逐条 RegExp 全局匹配)与称谓规范——称谓仅在 speechStyle.title 文本中含「不用…小姐 / 小姐…禁用 / 禁用…小姐 / 不称…小姐」这类否定式时才扫描 XX小姐。
审计报告落盘。扫描模式把候选写入 <root>/.novel-writer/audits/<book>-<sha1(book|章节文件名逗号串)[:16]>.json,内容为 { book, generatedAt, candidates };文件名中的哈希只由书名与章节文件名列表决定,正文改动不换文件,增删章节才换。
约束与边界
- 三种哨兵模式互斥。同传 ≥ 2 种直接抛错,不做优先级择一;调用方必须拆分调用。要同时得到衔接与设定表结论时,先跑无参数扫描,再单独跑
chapter。 - 第一章的衔接检查是空结果,不是「通过」。目标章在排序后位于第 0 位时提前返回
candidates: [],且该返回体不含chapter字段,advice明确写「第一章无上一章可比对」。 - 候选条数与章节列表都有硬上限。每个候选的
chapters最多 5 个文件名(CANDIDATE_CHAPTER_LIMIT),超出时detail追加「(共 N 章,仅列前 5)」。数字口径候选总数上限 20。例外:人物缺失 / 人物缺场两路同样是前 5 章,但不追加该提示文案——不能靠有没有该文案判断是否被截断。 - 三路衔接检查会被静默跳过。语义距离一路需要
semanticStyle功能开启且本地模型文件在位(加载失败有 30 秒冷却后重试);人物延续一路需要设定表登记了长度 ≥ 2 的角色名;钩子承接一路需要novels/创作资料/<书>/钩子记录.md里有与上一章章号相同的方向行。任一前提不成立时该路不产生任何候选——「没报语义距离」不等于「语义衔接良好」。 - 唯一不静默的降级是整章读不出来。章节文本读取抛错时反向透出「衔接·检查跳过」候选,并在
detail内附原因前 120 字,说明时间 / 人物 / 钩子三路本次全部跳过。 - OOC 是统计哨兵而非判定。采样窗口固定为角色名前后各 120 字、每章最多 5 处,短场景与对白密集章的代表性有限;出现章数 ≤ 4 时改用固定阈值 0.3(小样本的 σ 会被离群值撑大)。
detail会写明本次用的是固定阈值还是 2σ,并在文案中提示「若有重大剧情触发情绪变化可忽略」。 - 角色过滤口径两种模式不一致。OOC 模式要求
name.length ≥ 2;全书扫描的人物缺场检查只要求name !== ""。单字角色名只在后者参与检测。 - 大纲对照是字面重合度,不是语义对照。只看方向行的相邻二字组在该章前 600 字中的命中比例,阈值 0.12;方向文本长度 < 4 的方向行、章号对不上的方向行都被跳过且不计入「跳过」计数。返回值只是「可能偏离」提示,偏离本身不一定是错。
- 章号对齐用解析结果,不取文件名首段数字。大纲与钩子两处都走
parseChapterNumber,因此第1卷03章.md按章号 3 匹配;全角章号(第01章)先做宽度归一。 - 扫描模式只读正文与设定表,唯一写入是审计报告。报告写盘失败不阻塞执行;但
reportFile字段在写盘前就已算出,写入失败时返回值里仍会出现这个路径,该文件此时并不存在。 - 未登记 worldview 的影响是双向的。「查不到禁用词表」不代表「不扫描」:会先用
detectCulture判定全书文化基准再选词表(eastern → 中式禁词、modern → 跳过、其余 → 默认欧式基准)。显式登记bannedWords: [](「不要任何禁用词」)与「未登记」语义不同,前者会直接使用空词表。 - 数值与条数不可跨版本横比。v4.3.0 把候选章节列表统一截断到 5(此前用语 / 客套 / 仪式 / 称谓四路会把全书章节名全部塞进候选)、数字口径改为按数值分组去重(此前任意两两配对会产生 N(N−1)/2 条候选)、衔接模式的读取失败不再吞成空数组、书名在
detectChapterBridge入口统一净化。同一本书在不同版本下的候选条数不可直接比较。 - 语义距离的引擎状态与
novel_semantic_search同源。该路复用embedding.isAvailable/embed/cosine,与索引检索无关,是两次单段嵌入;仅对 600 字与 800 字文本做一次余弦,不落盘、不进索引缓存。
相关工具
- novel_settings —— 五张设定表的维护入口;本工具的人物缺场 / 别名 / 设定重复 / 用语与语用五路全部读它。
- novel_outline —— 创作资料(含
剧情大纲.md与钩子记录.md)的初始化与回填;大纲对照与钩子承接两路依赖它写入的- N方向行。 - novel_summary —— 章节摘要;长书审计后按摘要复核候选比逐章重读省 token。
- novel_chapters —— 章节清单;
chapter参数取值先用它确认。
源码位置
- lib/index.js —— 工具注册与四种模式的实现:
registerNovelContinuityCheck、capCandidateChapters、CANDIDATE_CHAPTER_LIMIT - lib/core.js ——
detectChapterBridge(衔接四路)、TIME_JUMP_WORDS、readSettings、findChapter、parseChapterNumber、detectCulture、normalizeSettingList - lib/analysis.js ——
valenceStats/valenceSeries(OOC 的情绪效价与否定过滤) - lib/embedding.js ——
isAvailable/embed/cosine(语义距离一路) - lib/lexicons/markers.js ——
DEFAULT_BANNED_WORDS/SPEECH_STYLE_RULES/CULTURE_MARKERS - mcp/server.mjs —— MCP 侧的
root注入与越界回退