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

novel_keywords

高频关键词统计。对某部作品或单个章节做纯字面计数,产出四类条目:中文相邻二字组、中文相邻三字组、疑似人名/专名、英文词。输出不含单字——单字频次不参与统计,只保留重复出现的二字组、三字组与英文词,以及出现两次以上的疑似人名。全程零依赖、无分词模型、不调用在线大模型,结果同时落盘到书库数据目录留档。

参数

参数类型必填说明
bookstring书名,即 novels/ 下的子目录名。经 sanitizeSegment 过滤。目录不存在或目录下无可识别的章节文件时直接报错,不返回空结果。
chapterstring只统计该章节;省略则统计全书。取值可以是章号(1 / 01 / 第一章)、文件名或标题子串,解析规则见「约束与边界」。
topinteger返回的关键词数量,须为 1–100 的整数,默认 20。越界即报错,不截断到边界。
rootstring章节库根目录。缺省回退顺序:args.rootconfig.root → 会话工作目录。MCP 形态下必须落在启动参数 --root 之内。

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

输出结构

契约字段(output.schema.required):book / scope / totalChars / keywords 四项必返。

字段类型内容
bookstring必返。清洗后的书名
scopestring必返。统计范围:单章模式为章节文件名;全书模式为 全书 N 章
totalCharsinteger必返。各章原文长度之和(chapterText.length 累加,含 Markdown 标题行与空行)
keywordsarray必返。按 count 降序排列的条目数组,最多 top 条,可能为空数组
keywords[].wordstring必返。词面
keywords[].countinteger必返。出现次数
keywords[].kindstring必返。枚举 cjk-bigram / cjk-trigram / name-candidate / word,分别对应二字组、三字组、疑似人名、英文词
reportFilestring报告落盘的绝对路径。落盘失败时不返回该字段(主流程不受影响)

计算原理

全部统计为单遍字面扫描,没有词表以外的语言模型,也不做词性标注与切词。四种条目各自独立计数,最后合并、排序、截断。

1 · 语料拼接与字数口径

全书模式:selected = scanChapters(dir) ← 全部章节,章号升序,不可解析者排最后 单章模式:selected = [ findChapter(chapters, chapter) ] text = 各章原文以 "\n\n" 连接 totalChars = Σ 各章 chapterText.length ← 不含插入的分隔符

章节之间强制插入空行是 v4.0.0 的修正:旧版首尾相接会把上一章末尾与下一章开头当成相邻二字/三字组,产出正文里根本不存在的「词」。因此全书统计的结果不等于各章分别统计后求和。另需注意 totalChars 是原文长度累加,包含 Markdown 标题行与空行,与 novel_style_report 先剥离标题行再统计的口径不同,两个工具的同一字段不可互相校验。

2 · 中文相邻二字组与三字组

切分:文本按字符扫描,/[一-鿿]/ 之内累积为「连续汉字串」, 标点、空白、拉丁字母、数字一律作为断点(断点不参与任何组) 对每个长度为 L 的汉字串,取两套重叠窗口: 二字组:位置 i 与 i+1,若两字不全在停用字表 → 计数 +1 三字组:位置 i 与 i+2 共三字,若其中至少 2 字不在停用字表 → 计数 +1 CJK_STOP_CHARS 单字停用表 = 68 个字符(去重后 67 字,含重复的「有」): 的了是在有和就都而于及与或这那之其以被把让向着过也还又但更最很从对为等 啊吧呢吗嗯哦呀哈啦么个中上下前后左右来去出进到说想看要会能可没有不

停用规则是「组内至少一个字是有信息量的实字」,用来压掉「的了」「是在」这类纯虚词碎片。取舍代价是重叠计数:同一段文字会同时产出二字与三字条目(例如「琉璃说道」会产出「琉璃」「璃说」「琉璃说」等),cjk-bigramcjk-trigram 的计数不可相加,且三字组常为跨词边界的碎片,不应直接当作词汇表使用。

3 · 英文词

text.toLowerCase().matchAll( /[a-z]{2,}/g ) 命中词不在 EN_STOP_WORDS(101 个英文停用词:the and of to in a an is are was were …) → 计数 +1

单个字母不参与统计(下限 2 个字母)。英文词在源头按出现计数,最终入选阈值是 count > 1,因此只出现一次的英文词不会出现在结果中。

4 · 疑似人名 / 专名

模式 A(2–3 字懒惰匹配 + 动作词): /([一-鿿]{2,3}?)(?:说|道|问|喊|叫|想|笑|叹|答|喝|骂|念|哭|点头|摇头)/g 模式 B(2–3 字贪婪匹配 + 称谓词): /([一-鿿]{2,3})(?:小姐|大人|先生|少爷|姑娘|殿下|老师|导师|婆婆|爷爷|奶奶|夫人|老爷|公子|长老|神甫|陛下)/g 过滤:以 他 她 我 你 它 又 再 还 这 那 谁 起头的命中一律丢弃 整词黑名单:那个 这个 什么 怎么 自己 她们 他们 你们

模式 A 的量词取懒惰是为修正贪婪误吞:贪婪会把「琉璃说道」匹配成「琉璃说」加剩余部分,使人名候选被动词尾巴污染。代词起头过滤针对「他说」「她又」「我问道」这类伪人名。该通道本质是启发式,故输出的 kind 标为 name-candidate(疑似)而非确定人名。

5 · 入选阈值、排序与截断

入选阈值: cjk-bigram count > 1 cjk-trigram count > 1 name-candidate count ≥ 2 word count > 1 排序:count 降序;count 相同时按 word 的 localeCompare 升序(结果稳定可复现) 截断:slice(0, top) top 默认 20,取值区间 1–100

出现一次的词组在合并阶段就被丢弃,因此「篇幅可观但用词分散」的文本可能返回空数组——渲染层此时输出「(未提取到重复出现的关键词)」。空数组是正常结果,不等于统计失败。

6 · 报告落盘与保留份数

reportFile = <root>/.novel-writer/analysis/ <book>-keywords-<sha1( book + "|" + scope + "|" + text ) 前 12 位>.json 写入内容 = 返回体 + generatedAt( ISO 时间戳 ) + ver( 插件版本号 ) 写入方式 = 原子写(临时文件 + rename,失败回退直写) 保留策略:同书的关键词报告最多保留 20 份(按 mtime 降序),更旧的删除

指纹含内容哈希与 scope:改稿或切换统计范围都会生成新文件,同长度改稿不再互相覆盖(v4.0.0 修正,旧版只哈希 text.length)。落盘失败被 try/catch 吞掉,仅表现为返回体缺少 reportFile。该文件只写不读:本工具不会因缓存存在而跳过计算,每次调用都重新统计;需要复用历史结果时由调用方自行读取该路径。

约束与边界

  • 四类条目共用同一个 top 预算。排序对合并后的全部条目全局进行,不按 kind 配额分配:top 较小时结果可能被某一类(通常是高频二字组)占满,其它类别被整体挤出。需要分类观察某一类时应提高 top(上限 100)后按 kind 过滤,或改用单章缩小语料。
  • 字面统计,无分词。没有词性标注与词典切词:三字组常为跨词碎片,二字组也可能跨越词边界(「琉璃说」)。cjk-bigram / cjk-trigram 是「字面重复片段」而不是词汇表,不应直接当名词表使用。
  • 输出不含单字。单字频次不参与统计;若确需单字分布,应改用句式分析或风格报告工具,而不是通过降低 top 或放宽参数来绕过。
  • 阈值不可配置。入选阈值(>1 / ≥2)与停用字表是代码内常量,没有对应的工具参数;不同插件版本的停用表若发生变化,同一本书的关键词列表不可跨版本直接比较。
  • 与风格工具的计数口径不同。totalChars 为原文长度累加(含 Markdown 标题行),风格报告侧先剥离 ^#{1,6} 标题行再统计,且两者统计对象(字面片段 vs 六维密度)不同。同名数字字段之间不存在可比关系。
  • 章节解析走 findChapter:v4.3.1 起先按精确文件名匹配(含省略扩展名,如 第01章.md第01章),再按章号数值匹配(1 / 01 / 第一章 同义),最后按章节标题或文件名的子串匹配。同章号存在多个文件时按章号定位会报错并列出候选,需改用文件名。纯数字参数不做子串兜底——传入 "1" 不会命中「第11章」,只会在数值相等时命中。找不到章节时报错,不降级为全书统计。
  • 报错而非空结果的情形:书名目录不存在(提示先用 novel_books 核对作品名)、目录下没有 .md / .markdown / .txt 章节文件、chapter 无法匹配、top 不在 1–100 的整数区间。
  • 只读顶层文件。章节扫描不递归,子目录中的稿件不会进入统计,也不会计入 totalChars
  • 报告目录的清理边界。保留 20 份的清理只匹配 <book>-keywords-*.json,同目录下的 <book>-full.json<book>-chapters-metrics.json 等分析缓存不受影响。
  • root 与工具开关:MCP 形态下 root 越界会被静默回退书库根;execute 首行执行 assertToolEnabled,UI 中关闭该工具后调用直接报错。

相关工具

  • novel_style_report —— 六维风格画像,其中的词汇维度与本工具同属字面统计但口径不同。
  • novel_sentence_analysis —— 高频句式模板与题材/关键词共现,用于看「句式怎么排」而非「哪些字在重复」。
  • novel_read —— 对可疑的三字组碎片回到正文核对上下文。

源码位置

  • lib/index.js —— 工具注册与 execute:参数 schema、输出契约、语料拼接、报告落盘与保留清理(registerNovelKeywords,约 2782 行)
  • lib/core.js —— extractKeywords 四类条目统计与排序截断(约 397 行)、tallyCjkRun 汉字串滑动窗口(约 446 行)、CJK_STOP_CHARS 单字停用表与 EN_STOP_WORDS 英文停用词(约 187 行)
  • lib/core.js —— scanChapters / findChapter 章节定位、sanitizeSegmentatomicWriteJson 原子写、novelDataDir 数据目录、formatKeywords 渲染
  • lib/index.js —— pruneAnalysisReports 同书保留最近 20 份报告的清理(约 155 行)
实现文件的归属易被误判。四类关键词统计不在 lib/analysis.js:该文件服务于句式分类、情感量化与风格指纹,与本工具没有调用关系。关键词的计数、阈值与排序全部位于 lib/core.jsextractKeywords / tallyCjkRun,工具壳层位于 lib/index.jsregisterNovelKeywords