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

novel_settings

设定管理。用 category 选定五张设定表之一(人物 / 地点 / 道具 / 时间线 / 世界观用语规范),以 action 执行增删改查;scan 从全书正文里按类别正则提取候选条目名,detect 用词表统计自动判定作品的文化基准(西方欧式 / 东方中式 / 现代都市 / 混合)。全部为本地规则统计与字符串写入,单部作品一份 JSON,五张表同文件存放。

参数

参数类型必填说明
bookstring书名,即 novels/ 下的子目录名。经 sanitizeSegment 过滤。
categorystring枚举 character / location / item / timeline / worldview。非枚举取值(含省略)一律按 character 处理;action:"detect" 时被强制为 worldview
actionstring枚举 list / add / update / delete / scan / detect。非枚举取值按 list 处理。
namestringadd / update / delete 的条目名;timeline 可用 day 代替。timeline 条目登记时 nameday 同值。
descriptionstring通用描述字段。
traitsstringcharacter 专用:性格 / 外貌特征。
relationshipsstringcharacter 专用:人际关系。
aliasstring[]character 专用:别名。
firstSeenstring首次出现的章节。
owner / status / lastSeenstringitem 专用:当前持有者 / 状态 / 最近出现章节。
daystringtimeline 专用:时间点标识。add 时作为条目名登记;update / deletename 定位旧标识,改名时 name 传旧值并同时传 day 新值。
event / chapterstringtimeline 专用:事件与对应章节。
notesstring备注。
basisstringworldview 专用:判断依据 / 文化基准说明,同时参与语用规范的自动推导。
ritualstringworldview 专用:仪式规范(如「点烛不烧香」)。
speechStyleobjectworldview 专用:说话方式规范。可含 title / tone / honorBad / honorGood / ritualBadPatterns / ritualGoodNote
bannedWordsstring[]worldview 专用:禁用词表。
recommendedobjectworldview 专用:替代词映射,如 { "上香": "点烛" }
rootstring章节库根目录。

action 六个取值的语义:list 查看(默认);add 登记;update 修改;delete 删除;scan 扫描章节提取候选条目名;detect 自动判断世界观文化基准(worldview 专用,会忽略传入的 category)。

输出结构

契约字段(required: ["book","category","action"])恒返;其余字段按动作出现,形态并不统一。

动作返回的字段说明
listtotal + characters locations items timeline worldview五张表全量返回,total 为五表条目数之和;无 message
add该类别对应的表 + message只含新增的那一条;表键由 category 决定
update该类别对应的表 + message只含更新后的那一条
deletetotal + 五张表 + message删除分支不提前返回,落到与 list 相同的收尾块,返回全表
scancandidates + 五张表 + message候选为字符串数组;不含 total
detectculture / confidence / scores / evidence / total / worldview / message,条件性带 genre / theme / vibe文化基准判定结果 + 世界观表
字段类型内容
characters / locations / items / timeline / worldviewobject[]五张表的条目数组,条目结构见下
totalinteger五张表条目数之和
candidatesstring[]形如「琉璃(7次)」,已过滤已登记名,最多 15 条
culturestringwestern / eastern / modern / mixed / unknown
confidencenumber0–0.95,保留两位小数
scoresobject{ western, eastern, modern } 命中计数
evidenceobject按文化分组的证据串数组,如 ["教堂×3"],每组最多 8 条
messagestring结果文案;detect 下形如「自动判断:东方/中式古代(置信度 82%)」
genre / themeobjectdetectgenreTheme 功能开关开启时出现
vibeobjectdetect 下的氛围光谱(axes / top / conclusion / confidence / evidence
渲染文本只显示 category 对应的那一张表。返回体的 JSON 里 list / scan / delete 都带五张表,但 output.rendervalue.category 取表:省略 category 时默认值为 character,MCP 客户端看到的文本就只含「人物卡(N 条)」。需要看其余四张表时必须显式传对应 category,或直接消费 JSON 返回体。

数据模型与落盘

{
  "book": "雨夜灯",
  "characters": [
    { "name": "沈砚", "traits": "左手旧伤,雨天隐痛", "relationships": "沈家次子",
      "alias": ["阿砚"], "firstSeen": "第01章", "notes": "随身带半枚铜钱" }
  ],
  "locations": [
    { "name": "西市当铺", "description": "砖墙剥落,柜台高于人肩", "firstSeen": "第07章" }
  ],
  "items": [
    { "name": "半枚铜钱", "owner": "沈砚", "status": "在沈砚处", "lastSeen": "第19章" }
  ],
  "timeline": [
    { "name": "第1天", "day": "第1天", "event": "沈砚返乡", "chapter": "第01章" }
  ],
  "worldview": [
    { "name": "东方/中式古代", "basis": "全篇出现客栈、掌柜、时辰",
      "ritual": "中式宗教仪式=焚香/上香/跪拜;禁点烛/弥撒/礼拜",
      "bannedWords": ["教堂", "神甫"],
      "recommended": { "教堂": "庙/寺" },
      "speechStyle": { "title": "中式称谓:老爷/夫人/姑娘/小姐/官人可用;禁 Miss/Mr.+名式的西式称呼",
                       "tone": "中式对话:可文言客套(老夫/在下/承蒙/赐教/小姐);禁西式口语(Ah/well/我的上帝)",
                       "honorBad": ["Miss", "先生", "阁下", "Thank you", "if you please"],
                       "honorGood": { "先生": "老爷/相公" },
                       "ritualBadPatterns": ["点烛", "蜡烛", "礼拜", "祈祷跪下?", "light candle"],
                       "ritualGoodNote": "中式宗教仪式=焚香/上香/跪拜;禁点烛/弥撒/礼拜" } }
  ]
}

字段结构取自 normalizeSettingEntry 的白名单;上例数值与文本为示意。

落盘位置

<root>/.novel-writer/settings/<book>.json,写入时固定为 { book, characters, locations, items, timeline, worldview }——五张表在同一份文件里。读取时逐表做 Array.isArray 校验,缺失或类型不对的表回落为空数组;文件不存在或 JSON 损坏时五表全空,不抛错。

五张表与 listKey 映射

category表键渲染标签专属字段
charactercharacters人物卡traits relationships alias
locationlocations地点卡description firstSeen
itemitems道具清单owner status lastSeen
timelinetimeline时间线day event chapterdayname 同值)
worldviewworldview世界观/用语规范basis ritual speechStyle bannedWords recommended

字段白名单不按类别强制:normalizeSettingEntry 对任意类别都保留全部已知字段,跨类别传字段不会被拒绝,只会原样落盘。字段值统一经 String() 强转(name 缺省写成空串)。

add 的写入与 worldview 语用规范自动推导

name 取值优先级为 args.nameargs.day,二者都空则报错。随后按固定键序把已在参数里出现的标量字段 String() 写入;alias / bannedWords 仅在是数组时写入;recommended / speechStyle 仅在是非 null 对象时写入。类别为 timeline 时额外写入 day = name

category:"worldview"未显式传入 speechStyle 时,插件按 basis 文本自动推导语用规范,判定顺序为:

① eastHint = /中式|东方|中原|中土|古装|古风|客栈|老爷|少侠|江湖|茶楼|媒婆|太监|衙门|香客/.test(basis) ② westHint = /west|西方|欧式|西式|教廷|教堂|神甫|公爵|骑士|城堡|女巫|庄园|圣器|牧师|弥撒/.test(basis) ③ detected = eastHint ? {culture:"eastern"} : westHint ? {culture:"western"} : detectCulture(basis)

判定为 easternwestern 时,从 SPEECH_STYLE_RULEStitleGuideline / honorBad / honorGood / ritualBadPatterns(正则对象的 source 字符串)/ ritualGoodNote / toneGuideline 六项写入 speechStyle,并在调用方未给 ritual 时用 ritualGoodNote 兜底。判定结果为 modern / mixed / unknown不写 speechStyle,也不写 ritual

「单一判定、一次赋值」是刻意设计。历史实现分成两块独立逻辑:先按裸「中/东」正则把「中世纪」误判为 eastern 并写入 speechStyle + ritual,第二块又按「欧/教堂」覆盖为 western,而 ritual 已落定 eastern 值,两个字段来源分裂。现版本三项提示(eastHint → westHint → 词表检测)互斥,只赋值一次。

update 的合并语义

  • 可改字段description traits relationships firstSeen owner status lastSeen event chapter notes basis ritual day(以及下方的数组 / 对象字段)。
  • name 不是可改字段,只用于定位。改名只能走 timelineday 通道。
  • alias / bannedWords 整体替换;recommended 整体替换;speechStyle 与已有对象浅合并{ ...old, ...new }),未提及的子键保留。
  • timelineday 变更时同步 name:原 name 等于旧 day 则跟随新值;name 为空则填新值;其余情况保持不动,避免身份字段分裂成 { name:"第1天", day:"第2天" }
  • 定位规则:先按 name 全等匹配;timeline 类别下额外按 day 全等匹配(双查)。找不到即报错,并提示 timeline 需用旧 day 定位。

scan 的候选提取

先拼读全书章节正文,再用类别专属正则提取候选,最后统一过滤与截断:

保留条件:count >= 2 且 该名不在本类别已登记条目名集合中 输出格式:"名字(N次)",slice(0, 15)
category提取正则(示意形式)附加过滤
character([\u4e00-\u9fff]{2,3}?)(?:说|道|问|喊|叫|笑|叹|点头|摇头)丢弃以「他她我你它又再还这那谁」开头的命中;丢弃「那个/这个/什么/怎么/自己/她们/他们/你们/我们」
location「回到/来到/走进/离开/路过/奔向/赶赴/躲进/藏进/出了/进了」+ 2–5 个汉字,且后随 [的里中前后处。,,!?!?] 或行尾丢弃「自己/他们/她们/我们/你们/这个/那个/什么」起头的命中
item「一把/一柄/一顶/一枚/一件/一块/一条/一盏/一封/一叠/几件/抓起/拿起/握紧/掏出/收好/放下」+ 2–6 个汉字丢弃「自己/他们/她们/我们/你们/这个/那个/什么/一点」起头的命中
timeline / worldview不适用「正文高频词」式扫描candidates 恒为空数组,message 改为指引用 add / detect

人物正则的量词是懒惰的({2,3}?),「琉璃说道」不会被吃成「琉璃说」。known 集合只取当前 category 那张表的条目名——跨表同名不会阻止候选出现。

detect 的文化基准判定

先拼读全书正文,再对三张文化词表(CULTURE_MARKERSwestern / eastern / modern)做一次扫描。扫描器 scanWordHits 按词长降序、用长度等于文本长度的占用位图消费命中区间:同表重复词不双计,同一位置的长词命中后其子串不再计数(「高潮迭起」命中则「高潮」不重复计)。

total = western + eastern + modern total = 0 → culture = "unknown",confidence = 0 modern >= 5 且 modern >= max(western, eastern, 1) → culture = "modern" 否则 western + eastern > 0: ratio = western / max(eastern, 1) ratio >= 2 → culture = "western" ratio <= 0.5 → culture = "eastern" 否则 → culture = "mixed",confidence = 0.5 命中分支的 confidence = min(0.95, 0.5 + 该文化得分 / (total × 2)) 返回前四舍五入到两位小数

modern 优先是为避免剧情里的西式道具词把现代都市题材误判为西式。detect 分支还会读取功能开关:genreTheme 开启时附带 genre(流派)与 theme(题材)检测结果;webnovelVibe 开启时把正文交给氛围光谱聚合,失败则降级为 { axes: [], top: [], conclusion: "气质聚合失败: …", confidence: 0, evidence: [] },不影响文化基准结论。

约束与边界

  • book 是唯一必填项required: ["book"])。categoryaction 都有隐式默认值:分别是 characterlist,且非枚举取值静默回退而不报错。传 category:"person" 不会失败,而是操作人物表。
  • detect 覆盖 category:返回体的 category 恒为 worldview,与传入值无关。
  • 同名条目不查重add 只做 list.push,没有名字去重;重复登记会产生两条同 name 的记录,后续 update / delete 只命中第一条(findIndex 取首次匹配)。
  • add / update / delete 的定位串是精确全等,不做去空格以外的归一:手写「沈 砚」与「沈砚」是两个条目。
  • 非数组的数组字段被静默忽略alias:"阿砚"(字符串)不会写入,也不会报错。同理 recommended / speechStyle 传字符串时按「未提供」处理,worldview 的自动推导分支照常执行;显式传 null 也被视为未提供。
  • scan 有硬阈值与硬上限:命中少于 2 次的名字不出现;最多返回 15 条;candidates 只是候选,插件不会自动登记,必须由调用方逐条 add
  • scan 读取的是全书正文:章节数多时 I/O 与内存开销随全书字数线性增长;scanChapters 在作品目录缺失时抛错,空目录则返回空候选列表。
  • detect 无章节时不报错:作品目录存在但没有章节文件时,正文为空串,判定结果为 culture:"unknown" / confidence:0message 为「自动判断:无法判断(词表未命中)(置信度 0%)」。目录不存在则会抛错。
  • score 与 confidence 不可跨版本横比:v4.3.0 清理了三张词表内部的重复词条(如「城堡」「便利店」「朋友圈」的二度出现)、v4.0.0 把扫描改成单遍占用位图,同一本书在不同版本下的 scores 会不同。去重本身不改变扫描语义(位图本就不双计),但历史版本的分数不可与新版本直接对照。
  • 并发是文件级的:v3.9.5 起 add / update / delete / scan / detect 全部包在 withFileTx 内(键为 settings 文件的绝对路径),同一本书的并发写被串行化,不会基于同一旧快照互相整表覆盖。代价是不同 category 的并发写也会互相排队——它们共用同一份文件。
  • 写盘是整文件覆盖 + 原子替换writeSettingsatomicWriteJson(临时文件 + rename,失败回退直写,两者都失败抛 ENOVELWRITEFAIL),并在成功后把书库根记入全局状态。
  • 列表动作渲染口径list / delete / scan 的返回体都带五张表,但渲染层只打印 category 对应的那一张(见上文警示块)。
  • 跨版本字段口径:v4.0.0 起 list 的渲染按 category 取表(旧版 ?? 链在 list / scan 恒返回全部五张表时永不前进,永远只显示人物卡);同版起 nullspeechStyle 不再被当作对象写入空壳,且「按 basis 自动推导语用规范」改为单一判定;脏条目(null / 标量)在 normalizeSettingEntry 里被过滤,旧版会在坏数据上抛 TypeError 打挂整个工具。
  • MCP 形态下的 root 限制:参数 root 必须落在服务器启动参数 --root 的书库根之内,越界会被静默回退,同时记 stderr。
  • 参数表禁止额外字段parameters.additionalProperties = false

相关工具

  • novel_continuity_check —— 消费本工具的五张表:登记人物的缺场 / 别名未用、设定表重复条目、按 worldviewbannedWordsspeechStyle 做的用语冲突与语用冲突扫描都在彼处实现。
  • novel_plot —— 伏笔登记表,可用 relatedCharacters / locations 引用本工具登记的条目名。
  • novel_outline —— 创作资料(创作设定 / 人物设定 / 剧情大纲 / 钩子记录 / 创作状态卡),与本工具的五张表是两套并行的资料体系。
  • novel_summary —— 章节摘要,与本工具同属「按书写入 .novel-writer/」的持久化数据。

源码位置

  • lib/index.js —— 工具注册与 executeregisterNovelSettings(约 1449–1759 行),含六个动作分支、speechStyle 自动推导与三套 scan 正则。
  • lib/core.js —— settingsFile / readSettings / writeSettings / normalizeSettingEntry / normalizeSettingList(约 1999–2054 行)。
  • lib/core.js —— detectCulture / detectGenre / detectTheme / scanWordHits / sortedMarkerWords(约 669–774 行)。文化基准判定与词表不在 lib/analysis.js:判定实现在 core.js,词表在 lib/lexicons/markers.js
  • lib/lexicons/markers.js —— CULTURE_MARKERS / SPEECH_STYLE_RULES / GENRE_MARKERS / THEME_MARKERS / DEFAULT_BANNED_WORDS(全文件 130 行,末尾统一导出)。
  • lib/core.js —— withFileTx(约 969 行)与 atomicWriteJson(约 942 行)。
  • mcp/server.mjs —— buildArgs / isInsideRoot(约 259–289 行)。