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

novel_lexicon

v6.3.0:本页描述的是修复后的最终行为。本版修掉了必用词表引入的一批缺陷:① novel_chapter_brief 此前不渲染 【必用词·优先使用】 分区(plan.checklist 的硬指令指向一个不存在的分区,avoid 勿用词一条都到不了模型),现已接上;② 词表文件损坏时,按层开关 / 保存会把整层条目静默清空,现改为拒写并明确报「词表文件损坏」;③ 词表面板「合并 / 全局」两个视图此前加载失败,现已修复;④ 改名撞车(newTerm 与该层已有条目同名)由静默合并改为明确报错。词表面板三层视图改用与「提示词档位」相同的官方分段控件,表单与列表改用其它面板一致的组件。

必用词表(优先用词)。把「某场景必须使用的专有名词 / 作者惯用词」登记成表,让写作时的用词从「AI 自己惯用的通用说法」变成「表内词」:动笔前由 novel_chapter_brief 作为硬清单注入,成稿后用 action: "audit"(只读)扫覆盖率与「通用词替身顶替」的行号与原句。两层存储——全局层放跨书惯用词,书级层放本书专名,同名条目以书级为准。工具本身不改正文、不生成文本。

参数

参数类型必填说明
actionstring是枚举 list / add / update / delete / import / scan / audit / export / toggle。唯一必填参数;非枚举值不回落,直接抛「未知 action」。
bookstring否书名(novels/ 下的子目录名),经 sanitizeSegment 过滤。scope: "global" 时可省略;其余写操作与 scan / audit 缺书名会报错。
scopestring否作用层:book(本书,写操作默认) / global(全局层) / all(两层合并,list 默认;update 时按「先书级、后全局」查找)。非枚举值按 book 处理。
termsstring[]否add / delete 用:词条数组,可一次几百条。add 时每项既是纯词条,也可写成 词|类型|场景|替身1,替身2|备注。# 开头的项按注释跳过(不登记)。
textstring否add / import 用:批量文本,每行一条,格式同 terms;# 开头为注释。
termstring否update 用:目标词条(精确全等定位);delete 用:要删的词条(等价于 terms 里的一个)。
newTermstring否update 用:把词条改成新名字。目标词条不存在时报错;新名字与同层已有条目同名时报错(除非显式传 overwrite: true,此时用旧条目覆盖同名条目)。
kindstring否类型:专名 / 偏好词 / 称呼 / 场景词 / 口头禅 / 其他(LEXICON_KINDS),也可自定义(落盘截 12 字)。决定开写包与渲染的分组标题。
scenestring否适用场景标签。本章大纲方向行里含该词时,该条在开写包里优先占位(排序权重最高)。落盘截 200 字。
notestring否用法 / 搭配说明。落盘截 200 字,注入与渲染时原样带上。
avoidstring[]否通用替身词:正文里出现它,即等于「没用必用词」。audit 据此报出行号与原句;开写包按每条 2(compact)/ 3(full)个注入。传字符串时按逗号 / 顿号 / 空格切分。
aliasesstring[]否等价写法(别名)。audit 同样算命中,不报「未使用」。切分规则同 avoid。
prioritystring否枚举 high / normal。只有 high 会被记录(其它值不落盘),作用是注入与审计排序靠前、渲染带 ★。
enabledboolean否toggle 用:整层开关;update 用:单条启停(false 的条目不再注入、不参与审计)。
overwriteboolean否add / import 用:同名词条是否覆盖(默认 true;传 false 时同名条目进 skipped)。update 改名撞车时也用它表示「允许覆盖同名条目」。
replaceboolean否import 用:true = 整层替换(先清空该层再写入);默认 false = 合并追加。
allboolean否delete 用:清空该层全部词条,必须显式传 true(只传 terms 空数组不会清空,而是报错)。
chapterstring否audit 用:章号 / 文件名 / 标题子串,或 all(全书 亦可);省略时审计最后一章。
querystring否list 用:子串过滤,命中范围为 term / kind / scene / note / avoid。过滤只影响 entries,不影响 counts。
limitinteger否list 用:返回条数上限,默认 200,钳到 [1, 1000]。scan 用:候选词上限,默认 60,钳到 [1, 400]。
offsetinteger否list 用:起始偏移,默认 0(须 ≥ 0)。越界时返回空页并在 message 里说明。
rootstring否章节库根目录。MCP 形态下越界值被静默回退到启动参数 --root 指定的书库根。

参数对象声明 additionalProperties: false。kind / scene / note / priority / avoid / aliases 六个字段会作为「公共字段」套用到本批全部词条上,但逐行解析出的值优先({ ...shared, ...parsed })。

action作用目标层必填
list查看词条(默认合并两层),支持子串过滤与分页all(默认) / book / globalaction;scope:"book" 时还需 book
add批量登记(terms 数组或 text 文本),可一次几百条book(默认) / globalaction + 词条
update改单条:改字段、改名(newTerm)、单条停用(enabled:false)book(默认) / global / all(先书级后全局)action + term
delete删除指定词条;all:true 整层清空book(默认) / globalaction + terms(或 all:true)
import文本批量导入(每行一条);replace:true 整层覆盖book(默认) / globalaction + text
scan从本书正文高频词提取候选,供挑选后 add—(读全书,只读)action + book
audit成稿用词审计:覆盖率 + 未使用清单 + 通用词顶替的行号与原句(只读)。只覆盖必用词表,不含世界观禁用词——禁词用 novel_continuity_check—(读合并两层词表)action + book
export导出某一层的全部词条 JSONbook(默认) / globalaction
toggle开 / 关某一层(enabled 必填布尔)book(默认) / globalaction + enabled

批量录入格式

每行一条,五列以 | 或 Tab 分隔,两侧空白自动裁剪;# 开头为注释行。只写第一列也能登记(其余字段留空或由公共参数补)。

# 本书专名(书级层):词|类型|场景|通用替身词|备注
星轨|专名|战斗|轨道,轨迹|跃迁场面统一用这个词
青铜灯|偏好词|夜巡|铜灯,油灯|主角随身物,别写成「提灯」
阿砚|称呼|日常|沈砚,砚哥|琉璃只在私下这样叫
北境哨塔

第四列的替身词按逗号 / 顿号 / 空格切分;第 5 列为备注。写成 词|类型 或 词|类型|场景 也成立——后面的列可以省略,中间不能跳过(想只给备注就得把前三列写满)。Tab 分隔与 | 等价,从表格或 Excel 里直接粘贴即可。

输出结构

契约必返字段(output.schema.required):action、message。其余字段按动作出现,形态并不统一;渲染层 formatLexicon 把返回体压成固定格式文本。

动作返回的字段说明
listentries + counts + files + layers(合并层)或 enabled(单层)entries 是本页(分页后);layers 分列两层的开关——合并视图没有「那一层」的开关,旧版此处恒回 enabled:true,两层都关时仍渲染「本层开关:开」
add / importadded / updated / skipped + entries(前 50 条)+ counts + files + enabled三个统计数组装的是词条名;skipped 仅在 overwrite:false 且同名时非空
updateupdated(单个元素)+ entries(前 50 条)+ counts + files + enabled改名成功时 updated[0] 是新名;撞车覆盖时 message 追加「(原「X」已被覆盖)」
deleteremoved + counts + files + enabled一条都没命中即报错,不返回空删除结果
scancandidates + counts候选为对象数组 [{ term, count, exists }],exists 表示已登记
auditaudit + counts + files + lexiconFirstlexiconFirst 是插件级「必用词优先」开关的当前状态(旧版借用了 enabled 键,语义串味)。message 末尾固定追加职责边界说明(v6.4.0,见下文「审计口径」)
exportentries(该层全量)+ counts + files + enabled该层文件不存在时按空表导出,并在 message 里说明
togglecounts + files + enabledmessage 同时给出插件级 lexiconFirst 的当前取值,避免与层开关混淆
字段类型内容
entriesobject[]词条数组,元素 { term, kind?, scene?, note?, avoid?, aliases?, priority?, enabled?, scope? }。scope 取 book / global,是读取侧派生的字段,不落盘
countsobject{ book, global, effective }:两层各自的条目数与合并后的生效条数。不随 query 过滤变化
layersobject{ book, global } 两层的开关状态,仅 list 的合并视图返回
enabledboolean被操作那一层的开关(toggle 回读值;export 为该层状态;单层 list 为该层状态)
lexiconFirstbooleanaudit 专用:插件级「必用词优先」总开关
filesobject{ book?, global? } 词表文件的绝对路径,可直接给用户看
candidatesobject[]scan 的候选词 [{ term, count, exists }],按正文词频降序
auditobject{ scopeText, total, coveredCount, missingCount, coverage, covered[], missing[], avoidHits[] },见下文「审计口径」。只覆盖必用词表——不含世界观禁用词
渲染文本把「本层开关」与「插件级开关」分开写。单层操作打印 本层开关:开/关;list 合并视图打印 各层开关:本书 开 / 全局 关;audit 打印 「必用词优先」插件级开关:开/关。三者是三个不同层次的开关,改一个不动另两个。

计算原理

1 · 两层存储与合并视图

词表目录 = <书库根>/.novel-writer/lexicon/ 全局层 = lexicon/_global.json ← _global 是保留书号,不属于任何一本书 书级层 = lexicon/<书名>.json 合并视图 readLexicon(root, book): 全局层在前、书级层在后,同名以书级为准(Map.set 覆盖) 某层 enabled === false → 整层不参与合并 counts = { global: 全局条数, book: 书级条数, effective: 合并后条数 }

落盘结构为 { book, enabled, entries, updatedAt },写入走 atomicWriteJson(临时文件 + 原子替换),成功后把书库根记入全局状态(供 UI 定位数据目录)。读侧容忍空书名(退化为只读全局层),写侧要求显式书名——避免「以为在写本书、实际动了全局」。

2 · 条目归一 normalizeLexiconEntry

term = String(entry.term ?? entry.word ?? entry.name).trim() term 为空 或 长度 > 60 → 整条丢弃(不污染词表) kind = 非空时落盘,截 12 字(可自定义,不限枚举) scene / note = 非空时落盘,截 200 字 avoid / aliases = 数组(或按 [,,、\s]+ 切分的字符串) → 逐项 trim、去空、去重,并剔除与 term 相同的项;为空则不落盘 priority = 仅 "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] → note

terms 数组里的字符串项与 text 的每一行走的是同一个解析器——注释行在两处都被跳过(历史缺陷:terms 曾用 ?? { term: raw } 兜底,把 #这是注释吗 登记成字面词条,并在开写包里当硬清单注入)。

4 · 合并追加 mergeLexiconEntries

existing 中已有同 term: overwrite !== false → 用新条目浅合并覆盖(进 updated) overwrite === false → 跳过(进 skipped) 新 term → push(进 added)

import 且 replace === true 时,合并基数为空数组——该层原条目被整体丢弃,这是唯一会「减少条目」的写入路径,用前请确认。

5 · 开写包注入(硬清单)

开关 lexiconFirst(功能开关,默认开)打开、且合并视图条数非空时,novel_chapter_brief 返回体新增 lexicon 字段,并在 plan.checklist 里追加一条硬指令。

lexicon = { enabled: true, total: 参与排序的启用条目数, shown: 本次注入条数, truncated: total > shown, groups: [ { kind, terms: [ { term, avoid[], scene?, note?, priority?, scope? } ] } ], counts: { book, global, effective }, files: { book, global } } 排序 selectLexiconForBrief(分数降序,同分保持登记顺序): score = (scene 命中本章大纲方向行 ? 4 : 0) + (priority === "high" ? 2 : 0) + (scope === "book" ? 1 : 0) 场景命中 → 高优先 → 书级条目先占位,其余按登记顺序补足到上限 注入上限 = BRIEF_BUDGETS.lexicon compact 60 条 / full 200 条 每条保留的 avoid 个数 = avoidPerTerm compact 2 个 / full 3 个

渲染文本的分区(render)形如:

【必用词·优先使用】凡涉及以下概念,必须用表内词写成;标了「勿用」的通用说法一个都不要出现:
〔专名〕
  - 星轨|(勿用:轨道/轨迹)|场景:战斗|跃迁场面统一用这个词
  - 青铜灯★|(勿用:铜灯/油灯)
〔称呼〕
  - 阿砚|(勿用:沈砚)
  (共 124 条,本章只注入 60 条;完整表用 novel_lexicon action=list)

同一次返回的 plan.checklist 里会多出一条(前 12 个词条列名,超出以「等 N 个」收尾):

□ 必用词优先(硬约束,完整表见上方【必用词·优先使用】):凡涉及这些概念,必须用表内词写成
  —— 星轨、青铜灯、阿砚 等 124 个;标了「勿用」的通用说法一个都不要出现

词条被截断时,degraded 追加一条说明总量与注入量;层被单独关闭时明确写「本书层已关闭(N 条)」;开关关闭但词表非空时写「已登记 N 条,但「必用词优先」开关当前为关闭」。

6 · 成稿审计 audit(只读)

对每一条启用词条(排序同开写包,上限 0 = 不限): forms = [term, ...aliases](去重,按词长降序匹配) count = forms 在正文各行的子串命中次数合计,重叠区间只计一次 (v6.9.5:登记「圣殿骑士」+ 别名「骑士」时,一行 1 次不再算成 2 次) count > 0 → covered { term, count, firstLine } count = 0 → missing { term, kind, scene, priority } 每条 avoid 词:正文任一行 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 章《标题》。

审计的职责边界(v6.4.0,方案甲:写清边界而非扩大范围)。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

text = 全书正文(逐章读取、章间补换行) keywords = extractKeywords(text, 400) ← 与 novel_keywords 同一套中文词组统计 candidates = keywords.map(k => { term, count, exists }).slice(0, limit) ← 默认 60,上限 400 exists = 该词已在合并两层词表中

候选不会被自动登记:scan 只回答「这本书正文里高频出现的是哪些词」,要不要收进词表由调用方决定。正文高频词往往正是作者的用词偏好,是补齐 avoid(作者不会用的通用说法)最容易的入口。

8 · 与世界观禁用词的分工

novel_settings 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 路由侧的 /lexicon POST 走同一把锁(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 读 / POST save·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、输出契约、开写包注入与开关门禁、场景命中排序、截断说明、审计只读性。