novel_lexicon
【必用词·优先使用】 分区(plan.checklist 的硬指令指向一个不存在的分区,avoid 勿用词一条都到不了模型),现已接上;② 词表文件损坏时,按层开关 / 保存会把整层条目静默清空,现改为拒写并明确报「词表文件损坏」;③ 词表面板「合并 / 全局」两个视图此前加载失败,现已修复;④ 改名撞车(newTerm 与该层已有条目同名)由静默合并改为明确报错。词表面板三层视图改用与「提示词档位」相同的官方分段控件,表单与列表改用其它面板一致的组件。
必用词表(优先用词)。把「某场景必须使用的专有名词 / 作者惯用词」登记成表,让写作时的用词从「AI 自己惯用的通用说法」变成「表内词」:动笔前由 novel_chapter_brief 作为硬清单注入,成稿后用 action: "audit"(只读)扫覆盖率与「通用词替身顶替」的行号与原句。两层存储——全局层放跨书惯用词,书级层放本书专名,同名条目以书级为准。工具本身不改正文、不生成文本。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
action | string | 是 | 枚举 list / add / update / delete / import / scan / audit / export / toggle。唯一必填参数;非枚举值不回落,直接抛「未知 action」。 |
book | string | 否 | 书名(novels/ 下的子目录名),经 sanitizeSegment 过滤。scope: "global" 时可省略;其余写操作与 scan / audit 缺书名会报错。 |
scope | string | 否 | 作用层:book(本书,写操作默认) / global(全局层) / all(两层合并,list 默认;update 时按「先书级、后全局」查找)。非枚举值按 book 处理。 |
terms | string[] | 否 | add / delete 用:词条数组,可一次几百条。add 时每项既是纯词条,也可写成 词|类型|场景|替身1,替身2|备注。# 开头的项按注释跳过(不登记)。 |
text | string | 否 | add / import 用:批量文本,每行一条,格式同 terms;# 开头为注释。 |
term | string | 否 | update 用:目标词条(精确全等定位);delete 用:要删的词条(等价于 terms 里的一个)。 |
newTerm | string | 否 | update 用:把词条改成新名字。目标词条不存在时报错;新名字与同层已有条目同名时报错(除非显式传 overwrite: true,此时用旧条目覆盖同名条目)。 |
kind | string | 否 | 类型:专名 / 偏好词 / 称呼 / 场景词 / 口头禅 / 其他(LEXICON_KINDS),也可自定义(落盘截 12 字)。决定开写包与渲染的分组标题。 |
scene | string | 否 | 适用场景标签。本章大纲方向行里含该词时,该条在开写包里优先占位(排序权重最高)。落盘截 200 字。 |
note | string | 否 | 用法 / 搭配说明。落盘截 200 字,注入与渲染时原样带上。 |
avoid | string[] | 否 | 通用替身词:正文里出现它,即等于「没用必用词」。audit 据此报出行号与原句;开写包按每条 2(compact)/ 3(full)个注入。传字符串时按逗号 / 顿号 / 空格切分。 |
aliases | string[] | 否 | 等价写法(别名)。audit 同样算命中,不报「未使用」。切分规则同 avoid。 |
priority | string | 否 | 枚举 high / normal。只有 high 会被记录(其它值不落盘),作用是注入与审计排序靠前、渲染带 ★。 |
enabled | boolean | 否 | toggle 用:整层开关;update 用:单条启停(false 的条目不再注入、不参与审计)。 |
overwrite | boolean | 否 | add / import 用:同名词条是否覆盖(默认 true;传 false 时同名条目进 skipped)。update 改名撞车时也用它表示「允许覆盖同名条目」。 |
replace | boolean | 否 | import 用:true = 整层替换(先清空该层再写入);默认 false = 合并追加。 |
all | boolean | 否 | delete 用:清空该层全部词条,必须显式传 true(只传 terms 空数组不会清空,而是报错)。 |
chapter | string | 否 | audit 用:章号 / 文件名 / 标题子串,或 all(全书 亦可);省略时审计最后一章。 |
query | string | 否 | list 用:子串过滤,命中范围为 term / kind / scene / note / avoid。过滤只影响 entries,不影响 counts。 |
limit | integer | 否 | list 用:返回条数上限,默认 200,钳到 [1, 1000]。scan 用:候选词上限,默认 60,钳到 [1, 400]。 |
offset | integer | 否 | list 用:起始偏移,默认 0(须 ≥ 0)。越界时返回空页并在 message 里说明。 |
root | string | 否 | 章节库根目录。MCP 形态下越界值被静默回退到启动参数 --root 指定的书库根。 |
参数对象声明 additionalProperties: false。kind / scene / note / priority / avoid / aliases 六个字段会作为「公共字段」套用到本批全部词条上,但逐行解析出的值优先({ ...shared, ...parsed })。
action | 作用 | 目标层 | 必填 |
|---|---|---|---|
list | 查看词条(默认合并两层),支持子串过滤与分页 | all(默认) / book / global | action;scope:"book" 时还需 book |
add | 批量登记(terms 数组或 text 文本),可一次几百条 | book(默认) / global | action + 词条 |
update | 改单条:改字段、改名(newTerm)、单条停用(enabled:false) | book(默认) / global / all(先书级后全局) | action + term |
delete | 删除指定词条;all:true 整层清空 | book(默认) / global | action + terms(或 all:true) |
import | 文本批量导入(每行一条);replace:true 整层覆盖 | book(默认) / global | action + text |
scan | 从本书正文高频词提取候选,供挑选后 add | —(读全书,只读) | action + book |
audit | 成稿用词审计:覆盖率 + 未使用清单 + 通用词顶替的行号与原句(只读)。只覆盖必用词表,不含世界观禁用词——禁词用 novel_continuity_check | —(读合并两层词表) | action + book |
export | 导出某一层的全部词条 JSON | book(默认) / global | action |
toggle | 开 / 关某一层(enabled 必填布尔) | book(默认) / global | action + enabled |
批量录入格式
每行一条,五列以 | 或 Tab 分隔,两侧空白自动裁剪;# 开头为注释行。只写第一列也能登记(其余字段留空或由公共参数补)。
# 本书专名(书级层):词|类型|场景|通用替身词|备注
星轨|专名|战斗|轨道,轨迹|跃迁场面统一用这个词
青铜灯|偏好词|夜巡|铜灯,油灯|主角随身物,别写成「提灯」
阿砚|称呼|日常|沈砚,砚哥|琉璃只在私下这样叫
北境哨塔
第四列的替身词按逗号 / 顿号 / 空格切分;第 5 列为备注。写成 词|类型 或 词|类型|场景 也成立——后面的列可以省略,中间不能跳过(想只给备注就得把前三列写满)。Tab 分隔与 | 等价,从表格或 Excel 里直接粘贴即可。
输出结构
契约必返字段(output.schema.required):action、message。其余字段按动作出现,形态并不统一;渲染层 formatLexicon 把返回体压成固定格式文本。
| 动作 | 返回的字段 | 说明 |
|---|---|---|
list | entries + counts + files + layers(合并层)或 enabled(单层) | entries 是本页(分页后);layers 分列两层的开关——合并视图没有「那一层」的开关,旧版此处恒回 enabled:true,两层都关时仍渲染「本层开关:开」 |
add / import | added / updated / skipped + entries(前 50 条)+ counts + files + enabled | 三个统计数组装的是词条名;skipped 仅在 overwrite:false 且同名时非空 |
update | updated(单个元素)+ entries(前 50 条)+ counts + files + enabled | 改名成功时 updated[0] 是新名;撞车覆盖时 message 追加「(原「X」已被覆盖)」 |
delete | removed + counts + files + enabled | 一条都没命中即报错,不返回空删除结果 |
scan | candidates + counts | 候选为对象数组 [{ term, count, exists }],exists 表示已登记 |
audit | audit + counts + files + lexiconFirst | lexiconFirst 是插件级「必用词优先」开关的当前状态(旧版借用了 enabled 键,语义串味)。message 末尾固定追加职责边界说明(v6.4.0,见下文「审计口径」) |
export | entries(该层全量)+ counts + files + enabled | 该层文件不存在时按空表导出,并在 message 里说明 |
toggle | counts + files + enabled | message 同时给出插件级 lexiconFirst 的当前取值,避免与层开关混淆 |
| 字段 | 类型 | 内容 |
|---|---|---|
entries | object[] | 词条数组,元素 { term, kind?, scene?, note?, avoid?, aliases?, priority?, enabled?, scope? }。scope 取 book / global,是读取侧派生的字段,不落盘 |
counts | object | { book, global, effective }:两层各自的条目数与合并后的生效条数。不随 query 过滤变化 |
layers | object | { book, global } 两层的开关状态,仅 list 的合并视图返回 |
enabled | boolean | 被操作那一层的开关(toggle 回读值;export 为该层状态;单层 list 为该层状态) |
lexiconFirst | boolean | audit 专用:插件级「必用词优先」总开关 |
files | object | { book?, global? } 词表文件的绝对路径,可直接给用户看 |
candidates | object[] | scan 的候选词 [{ term, count, exists }],按正文词频降序 |
audit | object | { scopeText, total, coveredCount, missingCount, coverage, covered[], missing[], avoidHits[] },见下文「审计口径」。只覆盖必用词表——不含世界观禁用词 |
本层开关:开/关;list 合并视图打印 各层开关:本书 开 / 全局 关;audit 打印 「必用词优先」插件级开关:开/关。三者是三个不同层次的开关,改一个不动另两个。
计算原理
1 · 两层存储与合并视图
_global 是保留书号,不属于任何一本书
书级层 = lexicon/<书名>.json
合并视图 readLexicon(root, book):
全局层在前、书级层在后,同名以书级为准(Map.set 覆盖)
某层 enabled === false → 整层不参与合并
counts = { global: 全局条数, book: 书级条数, effective: 合并后条数 }落盘结构为 { book, enabled, entries, updatedAt },写入走 atomicWriteJson(临时文件 + 原子替换),成功后把书库根记入全局状态(供 UI 定位数据目录)。读侧容忍空书名(退化为只读全局层),写侧要求显式书名——避免「以为在写本书、实际动了全局」。
2 · 条目归一 normalizeLexiconEntry
"high" 落盘(normal 与其它值都不写这个键)
enabled = 仅 false 落盘(true 不写这个键,即「默认启用」)normalizeLexiconList 再对同一 term 做后写覆盖前写(字段浅合并,保持首次出现顺序)。书级覆盖全局走的也是这条规则。
3 · 批量行解析 parseLexiconLine
# 开头 → null(跳过,不登记)
否则按 /\s*[|\t]\s*/ 切列:
cut[0] → term(为空则跳过)
cut[1] → kind cut[2] → scene
cut[3] → avoid(按 [,,、\s]+ 切分)
cut[4] → noteterms 数组里的字符串项与 text 的每一行走的是同一个解析器——注释行在两处都被跳过(历史缺陷:terms 曾用 ?? { term: raw } 兜底,把 #这是注释吗 登记成字面词条,并在开写包里当硬清单注入)。
4 · 合并追加 mergeLexiconEntries
import 且 replace === true 时,合并基数为空数组——该层原条目被整体丢弃,这是唯一会「减少条目」的写入路径,用前请确认。
5 · 开写包注入(硬清单)
开关 lexiconFirst(功能开关,默认开)打开、且合并视图条数非空时,novel_chapter_brief 返回体新增 lexicon 字段,并在 plan.checklist 里追加一条硬指令。
渲染文本的分区(render)形如:
【必用词·优先使用】凡涉及以下概念,必须用表内词写成;标了「勿用」的通用说法一个都不要出现:
〔专名〕
- 星轨|(勿用:轨道/轨迹)|场景:战斗|跃迁场面统一用这个词
- 青铜灯★|(勿用:铜灯/油灯)
〔称呼〕
- 阿砚|(勿用:沈砚)
(共 124 条,本章只注入 60 条;完整表用 novel_lexicon action=list)
同一次返回的 plan.checklist 里会多出一条(前 12 个词条列名,超出以「等 N 个」收尾):
□ 必用词优先(硬约束,完整表见上方【必用词·优先使用】):凡涉及这些概念,必须用表内词写成
—— 星轨、青铜灯、阿砚 等 124 个;标了「勿用」的通用说法一个都不要出现
词条被截断时,degraded 追加一条说明总量与注入量;层被单独关闭时明确写「本书层已关闭(N 条)」;开关关闭但词表非空时写「已登记 N 条,但「必用词优先」开关当前为关闭」。
6 · 成稿审计 audit(只读)
indexOf 命中即记一行,
但落在 term / aliases 命中区间内的一律跳过
(v6.9.5:登记「魔导步枪」+ 替身词「步枪」时,正文写对「魔导步枪」不再误报「用了通用替身词」)
avoidHits { term, word, line, snippet(该行前 46 字 + …) }
coverage = round(covered.length / total × 1000) / 10 ← 单位为 %
total = 0(词表为空)→ coverage 记 100,message 改为「先用 action=add 登记」chapter: "all"(或 全书)时逐章审计再合并:covered 累加各章命中数并附 firstChapter(首次命中的章),avoidHits 附 chapter,missing 剔除已在别章命中的词条;因此行号是该章文件内的真实行号,渲染成 第N章 Lx。省略 chapter 时审计最后一章,渲染成 第 N 章《标题》。
audit 只审必用词表——覆盖率与「通用替身词顶替」,回答的是「该用的词用上了没有」。它与世界观用语规范(novel_settings category=worldview 的 bannedWords / speechStyle)毫无关系:不读 worldview、不扫禁词、不报语用冲突。因此「覆盖率 100%」不等于「本章用词没问题」——「掌柜」「多谢提点」这类禁词命中不在本审计范围内,要查它们请用 novel_continuity_check(不带 chapter 审全书,带 chapter 只审该章)。这段边界文案由 LEXICON_AUDIT_SCOPE_NOTE 单一维护,同时写进 message、渲染文本的 【用词审计】 节尾与工具的 description;novel_continuity_check 的 checklist 里也有对应的反向一条(「必用词覆盖率不在本工具职责内」)。不选方案乙(让 audit 兼扫禁词)的理由:禁词扫描已由连续性审计承担(本轮起章节模式也会跑),两处实现同一判定必然出现口径分叉,且 audit 会被迫读 worldview 表、语义上从「用词审计」膨胀成「综合体检」,与它「只回答该用没用」的定位冲突。
渲染文本形如:
【用词审计】第 12 章《渡口》:必用词 24 条,命中 9 条(覆盖率 37.5%)
已用:星轨×4(首见 L12)、青铜灯×1(首见 L31)
未使用:阿砚、北境哨塔★
通用词顶替(该处本该用必用词):
- L44 「铜灯」→ 应写成「青铜灯」:他把铜灯搁在船头,火苗缩成一点。
注意:本审计只覆盖必用词表(覆盖率与通用替身词),不含世界观禁用词(novel_settings
category=worldview 的 bannedWords / speechStyle)——查禁用词请用 novel_continuity_check:
不带 chapter 审全书,带 chapter 只审该章。
7 · 候选词提取 scan
候选不会被自动登记:scan 只回答「这本书正文里高频出现的是哪些词」,要不要收进词表由调用方决定。正文高频词往往正是作者的用词偏好,是补齐 avoid(作者不会用的通用说法)最容易的入口。
8 · 与世界观禁用词的分工
category=worldview 的 bannedWords 管「不要写什么」(禁词),本工具管「必须写什么」(优先词)。两者互补、不重复:禁词为文化基准一致服务(欧式背景不写「上香」,中式背景不写「神甫」),由 novel_continuity_check 的用语冲突扫描消费;必用词为模仿作者的用词习惯服务(该写「青铜灯」而不是「铜灯」),由开写包注入、由 audit 复核。同一个词完全可以同时出现在两处(例如把「铜灯」写进 bannedWords、把「青铜灯」写进词表的 term),但常规做法是:禁词表只放「读者一眼能看出时代/文化错位」的词,词表放「本书专名与作者惯用词」,并在 avoid 里点出 AI 容易退回的通用说法。v6.4.0 起这条分工写进了返回体本身:audit 的 message / 渲染文本结尾固定追加边界说明,novel_continuity_check 的 checklist 则有「必用词覆盖率 / 通用词顶替——不在本工具职责内——用 novel_lexicon action=audit」一条,两个工具互相指路,单跑一个不会以为已经查全。
约束与边界
action是唯一必填参数(required: ["action"]),且没有隐式默认值——非枚举值直接抛「未知 action」。- 缺书名的后果按动作不同:
list默认scope:"all",无书名时退化为只读全局层(不报错);scope:"book"、add/import/update/delete/export/toggle/scan/audit缺书名一律报错「缺少 book 参数(只有 scope=global 的操作可以省略书名)」。 scope的默认值是「按动作」而非统一:写操作为book,list为all。update传all时先查书级、再查全局,只改第一个命中的层,不会两层同时改。- 定位是精确全等:
term/newTerm只做trim,不做其它归一——「青铜灯」与「青铜 灯」是两个条目。改名后原文里的旧写法不会被自动改写。 - 改名撞车必须显式确认:
newTerm与同层已有条目同名时,默认报错并说明「直接改名会与它合并、丢掉 aliases/priority/enabled 等字段」;只有传overwrite: true才用旧条目覆盖同名条目(返回message会注明「(原「X」已被覆盖)」)。 - 整层清空是显式动作:
delete必须给terms(或all: true);一条都没命中时报错并列出词条名,不会静默成功。要减少条目请用delete,不要用import replace:true的副作用。 - 词表文件损坏时默认拒绝写入:读盘区分「文件不存在」(
corrupt:false)与「内容读不出来」(corrupt:true+corruptFile(s),含 JSON 解析失败与权限/IO 类读取失败)。损坏层上执行add/import/update/delete/toggle会抛ENOVELWRITEFAIL,消息里给出文件路径与修复指引;要丢弃其内容重建必须显式确认——工具侧传force:true(或正常数据下用于同名词条覆盖的overwrite:true,仅在目标层已损坏时才等价于 force),UI 侧在二次确认后走POST /api/dsh-novel-writer/lexicon(overwrite:true/force:true)。这样「读成空表 → 写回空表」的清空路径就不存在了。
v6.6.0 起corruptError只给固定标签:解析失败时是「内容不是合法 JSON(解析失败)」,不再透出JSON.parse的原始 message。理由是那条 message 会回显文件开头若干字符(实测Unexpected token '这', "这是一段被误写成 json 的正文" is not valid JSON),而它原本经corruptError→corruptFields一路进到本工具的返回体与面板——也就是说,词表文件一旦被正文覆盖或写坏,正文片段就会出现在工具输出里。现在只说"是什么错",不说"错在哪段文字"(要看内容可以自己打开那个文件)。 - 写路径有文件级事务:五个写动作的「读—改—写」整体包在
withLexiconTx内(按涉及层的词表文件路径排序后依次加锁,scope:"all"会同时锁两层且不会与单层操作死锁),并发调用不会基于同一旧快照互相覆盖。HTTP 路由侧的/lexiconPOST 走同一把锁(v6.3.0 起:用与withLexiconTx相同的 file key 包住整段读—改—写)——界面上的 toggle / save / import 全部走这条路由,此前它与工具侧不互斥、两边同时写同一层会丢更新;现在「路由↔路由」与「路由↔工具」都串行化。 - HTTP 路由的请求体上限 1MB(超出返回
413)。几千条以内够用;更大批量请用工具的import分批导入。scope:"book"且书名为空时返回400(不会自动写全局层);scope:"global"时不校验book。 - 审计是子串匹配,没有词边界:
avoid词「轨道」会在「轨道车」里命中;条目「星」会命中「星期」。写词条时避免用过短或高频的通用字,必要时用aliases明确写法。 - 覆盖率的分母是词表条数,不是字数:
coverage = 命中词条数 / 词表条数。词表越大,覆盖率天然越低——它是「本章涉及的概念里有多少用对了词」的粗略指示,不是写作质量分数。 audit只读、绝不改正文,也不生成替换后的句子:它只回答「哪一行、哪个通用说法、本该写成哪个表内词」。改写仍由作者或宿主模型完成。audit的覆盖面是「必用词表」,不是「用词规范」。它不读worldview、不扫禁词与语用表达(v6.4.0 起把这条边界固定写进message与渲染文本)。「覆盖率 100% / 未发现通用词顶替 ✓」只说明表内词该用的都用上了;「这章有没有写出欧式背景不该出现的词」是 novel_continuity_check 的用语冲突扫描要回答的问题(该工具的checklist会明确列出这条分工)。- 注入与开关的层次:
lexiconFirst(插件级,默认开)关掉后只是不注入、audit 也提示未生效,词表自身的增删改查、导入导出、层开关照常可用;书级层与全局层各有独立开关,关掉某一层只影响该层是否参与合并视图与注入。 lexicon字段在开写包里「有就返回、没有就整键省略」:开关关闭、词表为空、或读取失败时该键不出现(读取失败会在degraded里写明原因)。调用方读它前必须先判存在性。- 截断只影响注入,不影响词表:compact 60 / full 200 条与每条 2 / 3 个
avoid都是返回体预算,词表本身与list/export的取数不受影响。 scan读全书正文:I/O 与内存开销随全书字数线性增长(与novel_keywords同源);作品目录不存在时抛错,空目录返回空候选。- MCP 形态下的 root 限制:参数
root必须落在服务器启动参数--root的书库根之内,越界会被静默回退并记 stderr。 - 界面入口:侧边栏「必用词表」页(三层视图 合并 / 本书 / 全局、当前书下拉、单条增改、批量导入、搜索、逐条删除、本层开关、词表目录入口),设置页「功能开关」里的
lexiconFirst行,工具页的novel_lexicon行都指向同一份数据;GUI 走GET /api/dsh-novel-writer/lexicon?book=&scope=all|book|global&q=与POST({ op: "save" | "import" | "toggle", scope, book?, entries? | text? | enabled?, replace?, overwrite? }),与工具侧共用同一套归一与合并逻辑。
相关工具
- novel_chapter_brief —— 消费本工具的主路径:把词表作为硬清单注入(
lexicon字段 +【必用词·优先使用】分区 +plan.checklist硬指令)。 - novel_settings —— 世界观用语规范:
worldview的bannedWords/speechStyle管「不要写什么」,与本工具的「必须写什么」互补。 - novel_continuity_check —— 消费设定表的禁用词做用语冲突扫描;词表不参与该扫描(词表的问题由
audit回答)。 - novel_keywords —— 与本工具的
scan同源(extractKeywords),前者给全书关键词报告,后者给「可登记为必用词」的候选清单。 - novel_sentence_config —— 功能开关
lexiconFirst(默认开)的读写入口。
源码位置
- lib/core.js —— 词表数据层:
LEXICON_KINDS/GLOBAL_LEXICON_BOOK/lexiconDir/lexiconFile/isGlobalLexiconScope/parseLexiconLine/classifyLexiconReadError/readLexiconFile/readLexicon/writeLexicon/normalizeLexiconEntry/normalizeLexiconList/lexiconEntryLine/mergeLexiconEntries/selectLexiconForBrief/groupLexiconByKind/auditLexicon(约 2470–2760 行)。 - lib/core.js —— 宿主 UI 路由:
lexiconHandler(约 1398–1550 行,GET 读 / POSTsave·import·toggle,含损坏层的409拒写);路由注册{ path: "/api/dsh-novel-writer/lexicon" }(约 1680 行);数据目录占位lexicon-dir(约 1590–1605 行)。 - lib/index.js —— 工具 action:
registerNovelLexicon(九个动作的参数 schema、分支与返回体)、formatLexicon(文本渲染)、LEXICON_AUDIT_SCOPE_NOTE(v6.4.0:audit 职责边界文案的单一事实源,message与渲染共用)、mergeChapterAudits(全书审计的逐章合并)、withLexiconTx(写路径文件事务);开写包 render 的【必用词·优先使用】分区也在本文件(novel_chapter_brief的render内)。 - lib/brief.js —— 开写包注入:
BRIEF_BUDGETS的lexicon/avoidPerTerm两档上限、lexiconFirst开关判定、两层开关与截断的degraded文案、plan.checklist的硬指令。 - lib/prompts.js —— 系统提示词里的「必用词优先」纪律(通用档与写作 / 审计 / 建资料三个场景各自的分工表述)。
- lib/client.js —— 侧边栏「必用词表」页(作用层分段 / 搜索 + 词条列表 / 「新增词条」展开表单 / 批量导入折叠区 / 逐条编辑与删除 / 层开关 / 底部「高级」里的书库根 / 「存储位置」折叠区)与两枚线图标
navLexicon/lexiconFirst(图标源共 52 个键)。 - test/lexicon-test.mjs —— 14 个用例:行解析归一、两层合并与覆盖、九个 action、输出契约、开写包注入与开关门禁、场景命中排序、截断说明、审计只读性。