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

novel_fix_plan

v6.1.0:plan 仍最多展示并保存 12 项;verify 使用未按该上限截断的检测全集比较,不把被高优先级问题挤出的旧项误判已解决。
v6.1.0:章节参数先精确匹配完整文件名,再匹配省略扩展名的文件名;同名多扩展名时必须提供完整文件名,明确指定却不存在的扩展名不会改读另一格式。
v6.5.0:字面类待办(禁用词 / 语用不符)改为行号区间定位,并跳过子串巧合(与 novel_continuity_check 的用语审计同一套邻接守卫);2 字词标为待判档,读原文后判定只是相邻巧合即可 mark state: "skip"。
v6.5.5:指标类待办的基线改为可配置——面板「风格基线」的基线范围(按全书 默认,=旧行为;按部分 只用勾选的章,按书存储)决定「跟谁比」。目标章永远排除(即使被勾选);实际参与 μ/σ 的基线章少于 3 章时容差下限抬到 ±25%;勾选的章一章都取不到时回退按全书(绝不以空基线出结论)。范围由 baselineScope(文案)/ baselineMode(档位)两个键披露,文案也写进 summary 与落盘清单。
v7.0.0:新增三类待办——AI套话(类别 8→9:同一类别下的多处命中合成一条,给命中处数与行号区间,target 是「0 处」,不罗列套话原文以免负样本诱发)、原地打转与大纲漏写(9→11,本地向量审计,只在改稿台跑)。

改稿台。把某一章的测量偏差翻译成按优先级排好序的待办清单:每项带稳定 id、类型、原文定位(lineStart / lineEnd / excerpt)、现值与目标区间、可照改的原著锚段、严重度与改写代价,以及一句改写方向。action: "plan" 出清单,"verify" 复测,"mark" 标记单项。工具本身从不生成改写后的正文句子。指标类待办「跟谁比」由面板的基线范围决定(默认按全书,可改成按部分、只用勾选的章)。

为什么要有它。novel_style_check 返回的是数据——偏差清单、六维判定、原著锚段与一个 verdict 分级;它没有说「所以先改哪一句」。把数据组织成行动是模型的额外工作,而工作一旦靠自觉完成,就会时做时不做。本工具把同一批测量结果直接翻译成待办:排序由 severity / effort 决定,位置由行号给出,改完用 verify 复测,形成 plan → 改 → verify 的闭环。
关键边界:hint 只给方向,不给正文。每项的 hint 是一句改写方向(如「这句抽象词过密,改成具体动作或物件」),而不是改写后的句子。生成正文是宿主模型的事,插件不做——这是本插件的立身之本:零 token、正文不出本机。任何把 hint 当作「可直接粘贴的成稿」的用法都偏离了契约。

参数

参数类型必填说明
bookstring是书名,即 novels/ 下的子目录名。空串或纯空白被拒绝,值经 sanitizeSegment 清洗。
chapterstring是要改的章节标识:章号(1 / 01)、文件名(第01章.md)或标题子串,由 findChapter 解析;找不到则报错。与 novel_style_check 同一套解析规则(文件名优先于章号、纯数字不做子串兜底)。
actionstring否枚举 "plan"(默认,产出待办清单) / "verify"(复测) / "mark"(标记单项)。省略或非枚举取值时按 plan 处理。
itemIdstringmark 必填mark 必需:目标待办项的 id(取自 plan 返回的 items[].id),必须已存在于该章已落盘的清单里。缺省或空白串直接报错。
statestringmark 必填mark 必需:处置状态,枚举 "done"(已按 hint 改过) / "skip"(有意不改)。缺省或非枚举值直接报错,且两种情形文案不同(v5.0.0 修正):未传 → 「缺少 state 参数:只能是 "done"(已改)或 "skip"(跳过不改)」;传了非法值 → 「state 只能是 "done"(已改)或 "skip"(跳过不改),收到:"finished"」。(旧版两种情形都报「需要 state 参数」,传了非法值时容易被误判成漏传。)
rootstring否章节库根目录。省略时按 config.root → 会话工作目录回退;MCP 形态下越界值被静默回退到启动参数 --root 指定的书库根。

参数对象声明 additionalProperties: false。本工具没有阈值类可调参数:严重度权重、类型判据与截断上限都是常量(截断上限恒为 12 条,v5.1.0 起由 40 收紧到 12)。测量入口 buildFixPlan / verifyFixPlan 自身接受 opts.tolerance / opts.maxItems / opts.plotStaleChapters 三个内部选项,但工具层固定传 {},不对调用方暴露。

action 取值

取值语义前置要求返回体形态
plan重新测量本章并产出待办清单(默认),同时把清单原子写到该章的 fix-plan-*.json。章节可解析;写作助手功能已开启(六维引擎门禁,与 novel_style_check 同口径)items[] + summary + planFile
verify对当前正文重算,并与上次落盘的清单逐项比对,给出三态结论。不写盘。同上(引擎门禁);清单文件不存在时不报错,改为报告「按当前正文现算」checked[](逐项三态)+ summary
mark把单项标记为已处置(state: "done" 已改 / state: "skip" 有意不改)。只改落盘清单里的处置状态,不重新测量、不改正文,因此不受六维引擎门禁约束。itemId 且 state 齐备,且该章已有落盘清单、itemId 能在清单里找到ok + item(被更新的那条)
门禁口径:mark 与 plan / verify 不同。plan / verify 依赖六维引擎,执行前先读状态文件里的「写作助手功能」总开关,关闭时直接拒绝执行(与 novel_style_check 完全一致)。mark 在引擎门禁之前就分支返回——它只是读-改-写自己那份清单,即使总开关被关闭也能用来登记处置状态。三个动作都仍要过工具级开关 assertToolEnabled("novel_fix_plan")。

输出结构

三个动作的返回体形状并不相同,且只有 plan 返回 planFile:

action返回字段
planbook、action、chapter({ file, number, title })、items[]、summary、planFile、baselineScope(基线说明)与 baselineMode(all / custom 档位)——v6.5.5 起两键一起返回、随面板「基线范围」变化,完整文案格式、与风格自检的同口径关系及残留的两处刻意差异见下文「十一类待办的判定来源」下的 v6.5.5 说明块
verifybook、action、chapter(优先取落盘清单里的 chapter,无清单时取本次现算的)、checked[]、summary。不返回 planFile(清单路径只在「找不到清单」时被写进 summary 文案)
markbook、action、summary、ok、item(更新后的那条,含 state / stateAt)

chapter 对象来自目录扫描:number 解析不出时为 null(键被整键丢弃)。契约必返字段是 book / action / summary。

items[] 条目

实现里的 makeItem 严格只产出下列 9 个字段,条目 schema 为 additionalProperties: false——落盘文件里额外追加的 state / stateAt 属文件内部字段,不出现在返回体。

字段类型内容
idstring稳定 id,形如 fix-<类型 slug>-<lineStart>-<sha1 前 8 位>。sha1 由 类型 | key | lineStart | lineEnd 派生(key 是指标名、word:词、honor:词、ritual:模式、title:sister、bridge:候选类型、plot:伏笔 id 等内部键)。同一章同一位置重新 plan 必得同一个 id,因此 mark 与跨次 verify 都能对齐同一项;正文改动导致行号变化时 id 随之变化。
typestring十一类之一(中文,取自 METRIC_TYPE 与各 collect*Items):句式偏离 / 抽象度过高 / 留白异常 / 禁用词 / 语用不符 / 衔接缺失 / 伏笔未回收 / 情感过直 / AI套话(v7.0.0)。
locateobject{ lineStart, lineEnd, excerpt }:段落的绝对行号区间(1 起,含空行计数)与该段原文节选(截 80 字)。lineStart = lineEnd = 0 表示该问题属全章 / 跨章层面、定位不到具体行。
currentstring当前实测值或命中描述。指标类为「维度名 + 实测值」(如 complexity 0.61);禁用词为「命中「词」N 处(位置:第2-10行,第52行)」;语用项为「客套禁词「词」N 处(位置:…)」/「仪式禁式「样本」N 处(位置:…)」/「称谓「XX小姐」(位置:第N行)」;情感过直为「情绪直陈 N 处(全章 M 处)」;衔接缺失为候选类型名;伏笔未回收为「未回收 N 章(最近提及:…)」。字面类的位置是可能不连续的多个区间(第2-10行,第52行);locate.lineStart / lineEnd 取首段起点与末段终点,是这些区间的超集——精确处看本字段。
targetstring目标区间或目标状态。指标类为基线带 low~high(如 0.38~0.52;基线 μ = 0 时为空串);禁用词为「改用「替代词」」或「避免使用该词」;衔接缺失为「补过渡/承接句(1 句即可)」;其余类别可能为空串。
anchorstring可照改的原著锚段(源自 buildStyleAnchorPackage 的抽样结果,与 novel_style_check 的 fixAnchors 同源;抽样语料同样按面板「基线范围」选取并排除本章)。只有指标类与 AI套话会带锚段(后者的 anchor 是一句类别说明,不是原著样例),其余类别为空串。
severityinteger严重度 1–5(超出范围会被钳到边界)。指标类按 |σ| 分档:≥ 3σ → 5、≥ 2σ → 4、其余出带 → 3;退化成「全章层面」的指标项再降一档(max(1, sev-1))。其余类别为固定值:禁用词 4、客套禁词 4、仪式禁式 4、称谓 3、情感过直 3、衔接缺失 2–4(时间硬跳 4 / 检查跳过 2 / 其余 3)、伏笔未回收 3–4(距离 ≥ 40 章或 priority: "high" → 4)。
effortinteger改写代价 1–3(超出范围会被钳到边界):1 = 局部替换即可(有登记替代词的禁用词 / 客套词;衔接缺失里的时间跳跃 / 时间过渡 / 钩子未接),2 = 需要重写该句或该段(指标类段落项、情感过直、仪式禁式、称谓;衔接缺失里的语义距离 / 人物断线),3 = 需要通读全章或跨章处理(指标类的全章层面项、伏笔未回收;衔接缺失里的检查跳过)。同严重度时先做省事的项。
hintstring规则拼接的改写方向(一句话,含实测值与基线的对照,指标类还会带上「本段 X」与方向建议),例如「抽象度偏离:全章 41(基线 22,偏差 +86%),本段 48。这句抽象词过密,改成具体动作或物件。只改这一段的写法,不要整章重写。」不含改写后的正文句子。
软化后缀(v5.1.0 新增,只有三类带):abstractDensity 抽象度 / gapIndex 留白指数 / hedgeDensity 不确定性三类指标的 hint,在原有前缀之后追加一句「(仅当这里读起来确实别扭才改;抒情、心理、留白段落属正常写法,可标记 skip 保留。)」。hint 前缀格式未变,只是句尾多这一句——它把这三维的默认处置从「改了」改成「先判断再决定」,抒情 / 心理 / 留白段落本来就该在这三维上偏离全书均值。
档位前缀(v6.5.0 新增,只有字面类带):禁用词 / 客套禁词的 hint 以 【硬命中·3字以上】 或 【待判·2字词】 开头。档位只在这一句里表达——type 字符串按契约不变(见「十一类待办的判定来源」)。待判档必须给出「可以不改」的出口:句尾追加「…请读第2-10行自行判断:若确为该词就改;若读该行发现只是相邻巧合,可跳过本项(mark state="skip"),并说明判断依据。」,否则会退化成一律照改。

排序

sort(items) = severity 降序 → effort 升序 → lineStart 升序 → id 字典序

四级排序的用意依次是:先改影响文风判定最严重的;同严重度时先做便宜的(不让人卡在难改的一段上);同权重同代价时按正文顺序自上而下,便于一次通读改完;最后一级只用于让完全同键的项结果可复现。lineStart = 0 的全章层面项排在同键项的最前(行号最小),调用方应显式分支处理,不要把它当作「第一段」。

渲染文本带 id(v5.0.0 修正):plan 的渲染在每条待办的「类型 / 严重度 / 难度 / 位置」行下方紧跟一行 id:fix-…。此前 id 只在返回体与落盘文件里,模型要 mark 得先自己去读 planFile 才能拿到——现在清单里直接可复制,少一次往返。

渲染文本带「处理建议」(v5.1.0 新增):plan 的渲染在 待办 N 条… 之后多一行处理建议,内容是分批改——同一段落 / 同一类型的问题合并成一次修改;一轮只处理 3~5 条;不必逐条 mark;改完最多 verify 一次。这是给调用方的节奏约束,不是新的返回字段:返回体形状未变(items[] 仍是 9 字段,state / stateAt 仍只落盘、只由 mark 写入)。改稿一次只动几处、一次改完整段,比按清单从上到下逐条打磨更接近真实改稿,也能避免「改一处、测一次」的往返成本把注意力耗在数字上。清单上有一条不等于必须改那一条:只有偏离达到 2 倍容差以上、或那一处读起来确实别扭时才动笔——容差带本身取 1.5σ,六维里有一维出带是常见现象,为对齐数字改文只会把作者的写法磨平;确实属于正常写法的(抒情 / 心理 / 留白段落)用 mark state: "skip" 留下记录即可。

十一类待办的判定来源

类型字符串是第一列这十一个中文词,没有其它取值。判定统一遵守文件头的硬约束:判定不出就跳过该类检查,绝不退化成默认词表兜底。

type判定来源典型触发 / 门槛
句式偏离六维中四个「写法维度」的出带判定(METRIC_TYPE 把 complexity / modifierDensity / actionDensity / hedgeDensity 统一映射到本类)该维相对偏差越出容差带,且绝对差值越过量级门槛:complexity 0.3(每句小句数)、modifierDensity 8、actionDensity 8、hedgeDensity 4(每千字点数)。低于门槛按「无量级差异」忽略
抽象度过高六维「抽象度」abstractDensity 出带同上,量级门槛 8(每千字点数)
留白异常六维「留白指数」gapIndex 出带同上,量级门槛 12(每千字留白点数)
禁用词词源由 core.resolveStyleRule 单点裁决(见下文「词表与语用来源」):去重(用户登记 ∪ 文化基准默认表) 后的正文逐行包含命中,再经邻接守卫过滤(命中处紧邻的前/后一个字落在实证碰撞表里即判为子串巧合,不产出待办);显式登记的 bannedWords: [] 表示「不要默认表」,此时只用用户表(可为空)。本书用词豁免(worldview.allowWords)里的词已在裁决点被静音,不会走到这里欧式背景作品出现「上香」一类用语冲突。每章每词最多一项,空串与重复词跳过。档位:3 字以上 = 硬命中(语料实测 0 误报);2 字词 = 待判(可能是「这[里正]是」这类相邻巧合,读原文可判无问题)
语用不符设定表 worldview.speechStyle(合并全部 worldview 条目后的结果,与 novel_continuity_check 同口径)的三路:honorBad(客套禁词,逐行精确包含 + 邻接守卫过滤,与禁用词同一套定位)、ritualBadPatterns(仪式禁式,按正则匹配,模式长度须 ≤ 200 字符,报全部命中行的区间,上限 200 处)、title(仅当规范明确禁用「小姐」时才扫 XX小姐,每章只报第一处)客套表达与已登记的文化基准不符(如中式背景用「提点」)。speechStyle.tone 不参与本类判定。客套禁词的档位口径与禁用词一致(2 字词 = 待判)
衔接缺失detectChapterBridge(上一章 → 本章) 的候选逐条转成待办(current 就是候选类型名);第一章没有上一章,整类跳过。检查本身抛错时也会产出一条 衔接·检查跳过 候选,不会静默等于「衔接正常」候选类型共 6 种:衔接·时间跳跃(硬跳词直接报)、衔接·时间过渡(软跳且上一章结尾无时间锚)、衔接·语义距离(本地 embedding 相似度 < 0.45,受语义开关与引擎可用性双重门控)、衔接·人物断线(上一章结尾出场人物在本章开头 800 字内全未出现)、衔接·钩子未接(上章钩子与本章开头字符命中率 < 25%)、衔接·检查跳过
伏笔未回收readPlots 中 status !== "done" 的条目:distance = 本章章号 − 最近提及章号,distance ≥ 20 的每条产出一项埋设已超过 20 章仍未回收;章号解析不出的条目跳过(宁可漏报),本章章号也解析不出时整类跳过
情感过直纯文本判定,不依赖设定表:全章「程度副词 + 情绪词」紧邻命中(中间只允许 0–2 字,可带一个「的/地/得」)总数 ≥ 3 次才报,只取命中数最多的前 2 个段落定位词表示例:「非常 / 十分 / 极其 / 极为 / 无比 / 格外 …」×「难过 / 悲伤 / 愤怒 / 高兴 / 恐惧 …」。单次出现视为正常行文(如「她非常高兴地答应了」)不报
原地打转(v7.0.0 · Phase 2)lib/vector-audit.js 的 detectSemanticLoop:chunkText 切语义块(≤150 字)→ embed() 取 512 维原向量(不复用 128 维降采样检索缓存)→ 相邻块余弦 → 章内 z 分数。判定要同时满足三条:连续 ≥3 块「稳健 z ≥ 1.8 且绝对余弦 ≥ 0.82」,并且这几块词汇零新颖度(新二字组占比 < 0.18)绝对余弦没有区分度(同章相邻块中位数 0.715、隔 20 块也有 0.632,只差 0.08),所以改判相对本章自己分布的 z 分数,再叠"没有新信息"这一条,避免把"同一处境换着词写"误当灌水。块太少(< 6)、引擎缺失、任一嵌入失败 → 整类跳过并在 summary 说明;块文本按哈希做内存 LRU,verify 时基本命中缓存
大纲漏写(v7.0.0 · Phase 2)把创作资料里该章的方向行按 ,;。、→ 切成子句(≤6 条),逐条判「关键词命中 || 向量相似 ≥ 0.42」:子句里的二字组只要在正文出现过就算兑现;否则用 BGE 检索前缀 "为这个句子生成表示以用于检索相关文章:" 加子句查向量,取与各块的最大余弦OR 合并是为了防误报:字面提到过就足以算兑现,向量只补"换了说法但确实写了"的情况;两者都没命中才报一条。找不到大纲文件或该章没有方向行时整类跳过(绝不猜)。locate 只到整章范围(做不到按段落定位),effort = 4(要动结构)
向量审计为什么只在改稿台跑(v7.0.0):实测每块 150 字嵌入约 47ms(万字章 3~5 秒)。开写包、 建章与常规 风格自检都是高频路径,多等几秒会直接影响可用性;改稿台是"用户主动要求细看"的场景,这几秒可以接受。 另外它受 features.semanticEmbedding 开关控制——关掉或本机模型不可用时明确跳过并披露,不会退化成"没发现问题"。
已知限制:它没有自己的开关。向量审计跟随「语义增强」这一个总闸(面板「功能」页 / novel_sentence_config 的 features.semanticEmbedding),所以有两头都被绑住的后果:① 你只想省掉改稿台这几秒,就必须连语义检索一起关掉;② 你关掉语义检索(比如为了省内存或加快检索外的其它工具),原地打转与大纲漏写这两类待办也会一起消失,而 summary 会写明「向量审计已跳过:语义嵌入功能已关闭」,不会假装它们不存在。要不要拆成两个开关,等真实使用反馈再定。
v7.0.0:「原地打转」的短章兜底改按字数。块数不等于字数——语义块按空行切、单块 ≤ 150 字,所以「< 11 块」往往只有 800~1500 字;而 2000 字的正常章常有 15~25 块,反而拿不到兜底。因此短章判据补了一条按字数的绝对兜底:
shortChapter = blocks < shortChapterBlocks(11) || ( 去空白字数 < shortChapterChars(2500) && blocks <= 16 ) 短章路径:相邻块余弦 >= shortSimThreshold(0.85) 即记为「像」 ← 仍须再满足「连续 >= 3 块」与 novelty 两道既有条件;命中后条目 mode 记为 shortChapter 常规路径:不满足 shortChapter 时仍走章内稳健 z 分数 + 绝对余弦 + novelty 三条件
这组阈值是经验值,未经语料标定:2500 字 / 11 块 / 16 块 / 0.85 都来自实测踩坑(z 分数在块少时样本太小),它只是兜底而非主判据——主判据始终是章内稳健 z 分数那一套。改阈值属源码常量变更(DEFAULTS.shortChapterChars / shortChapterBlocks / shortSimThreshold),面板不暴露。
「AI套话」的形状与口径(v7.0.0):按类别归并、给行号、目标是 0 处。每条的形状固定为:current = 「命中 N 处(位置:第 2-10 行)」(同类命中的行号区间,精确到每段的位置看这里)、target = 「0 处」、severity = 3~5(命中越多越高)、effort = 1(局部替换即可)。不罗列套话原文:这里只给类别与位置,避免把那一串 token 逐字写进提示词、反过来提高它被写出来的概率(负样本诱发)——生成侧(开写包)因此只给类别模式描述,同样不列原文。
v7.0.0:「AI套话」的两处豁免与一处边界修复。扫描前先由 buildSlopExemptions(基线章文本, { settings }) 生成豁免,两类情形不报:
① 题材静音:设定表 worldview 条目的 name / description / basis / notes 命中 SYSTEM_GENRE_RE(系统|无限|网游|数据面板|属性面板|游戏异界|数据流|面板流)时,整类静音 stat_numeric——在这类题材里数值播报是叙事语言,不是烂文笔。buildSlopExemptions 另有一个 genre 形参,但它只接受字符串;现有三处调用传入的都是原创设定对象 { worldview, characters, genre, … },按实现不参与判定——即题材判定实际只由设定表的世界观条目决定。
② 参照集高频习惯词豁免:参照集(本工具即基线章文本)合计 ≥ 500 字时,某条 pattern 在参照集里命中 ≥ 2 次且 ≥ 0.5 处/千字,就视为本书作者的个人习惯写法,逐条加入 exemptRules。章末升华(ending_preach)不参与这类豁免,已被题材静音或本就静音的类别同样跳过;两道门槛可由 minCount(默认 2)与 densityFloorPer1000(默认 0.5)覆盖,工具层不暴露。
③ 边界修复:endingLineSet 此前只按行号切段,于是「# 标题 + 一个空行 + 正文」这种落盘格式会把第 1 行也算进章末(「但他不知道」写在开篇却被当成章末升华)。现在先剔除标题行与分割线,且「全章没有空行、内容行 > 4」的单换行整章改按单行分段取最后 2 行内容行(≤ 4 行内容仍视作同一末段),与 splitBlocks 的分段口径一致。
这是「宁可漏报、不误报」的取舍:豁免只会让命中数变小、不会新增命中。因此豁免后的 total 与豁免前(或旧版本)的数字不可直接比——不要因为这一章的套话数比上一版少就认为文笔变好了;要判断是否真的变少,请用同一版本重扫两章。
「伏笔未回收」的阈值是 20 章(PLOT_STALE_CHAPTERS)。低于该距离的未回收伏笔不产出待办——长篇里同时挂着的线本来就有十几条,全部塞进清单只会让清单失去优先级。该类别按条产出(同章可以有多条伏笔待办),每条都指向跨章层面的问题,因此定位恒为 0/0,并在 hint 里请调用方在本章或近 2 章内回收、或用 novel_plot update 登记新进展。测量入口接受 opts.plotStaleChapters 覆盖该阈值,但工具层不暴露。
容差口径与 novel_style_check 一致(v6.2.0 起统一,v6.5.5 未改;本页旧文写「与之不同」,已更正)。两者都用排除本章后的基线自身的推荐容差(recTol = 作者自身章节波动的 1.5σ,经 core.buildTolerance 这一全插件唯一实现),而不是「含本章的全书基线」——含本章时,一个故意写坏的章节会把 σ 抬到 90%+,导致它自己反而不出带,改稿台就哑了。两者的差异只在样本门槛:本工具在可测基线 < 3 章时把容差下限抬到 25%、< 2 章时整类跳过;风格自检只要有 1 章基线就算(0 章时才返回 metric.enabled = false),小样本下限同样是 25%。
v6.5.5:基线语料由面板「基线范围」决定(按书配置)。按全书(默认)=旧行为;按部分 时基线只用勾选的章(不是「优先 + 不够再补」),且目标章永远排除——即使它出现在选中列表里。上一条「基线章节数 < 3 → 25%」的判定也按实际参与 μ/σ 的章数计算,所以只勾选 2 章(或勾了 5 章但有 3 章正文过短)时容差下限同样是 25%。
baselineScope / baselineMode 的文案(v6.5.5,照实现写)。两键一起返回:baselineMode 是档位(all / custom),baselineScope 是人类可读文案。回退后 baselineMode 也是 all,所以「本来就是按全书」与「回退」只能靠文案后缀区分。scopeText(n) 按档位分两式:
按全书:「全书除本章外的 N 章」+ 回退时按原因给后缀:「(自定义选择已失效,已回退全书)」或「(选中的章只有本章自身,不能作为自己的基线,已回退全书)」(原因由裁决点 resolveBaselineScope 回传的 reason 字段给出;此前这里只有第一种,把「只选到本章自身」也说成"选择已失效",是句错话,v6.5.5 已修)
按部分:「自定义基线:可测 N 章(选中 M 章)」,有失效章时括注为「(选中 M 章,其中 K 章已失效)」
无基线 / 不足:「无基线」「无基线(本章正文过短,指标类未判定)」「基线不足(可测量基线仅 N 章,至少需要 2 章)」
这里的 N 是「真正参与 μ/σ 的可测章数」,v6.5.5 起这与 novel_style_check 同口径(风格自检改成用可测章池再解析一次范围,只覆盖回报口径)。改造前两侧会不一致:勾了 12 章、其中 2 章正文过短时,本工具说「可测 10 章(选中 12 章)」而风格自检说「自定义基线 12 章(选中 12 章)」;现在两边都报 10 章。残留差异只有两处、都是刻意的:① 样本门槛——本工具可测基线 < 2 章即整类跳过,风格自检只要有 1 章就算;② 本章正文过短时,本工具会在文案尾部追加「(本章正文过短,指标类未判定)」。勾选的章若已改名 / 已删除会被跳过——它们仍计入「选中 M 章」,失效数由 K 报出(selected = 配置里选中的总数,missing = 其中已失效者)。本工具的池子是 scanChapters 的章节全集,所以这个差集只可能是「文件真的不存在」,用「已失效」是准确的(开写包与建章递的是可测池,那里的文案因此写作中性的「未纳入基线(已删除、或正文过短无法测量)」——两处措辞不同有理由)。全部失效时回退 按全书,绝不静默按空基线出结论。

locate:段落级 / 行号区间归因

现有分析引擎是段落级 / 句式级统计,没有行级归因。做法是先把正文切成段落并记下行号区间,逐段算指标,再把偏离最严重的若干段回映射成行号。三类问题的归因口径并不相同:

// ① 行号索引:换行先归一到 \n,1 起编号(与 novel_read 同口径) lines = text.replace(/\r\n?/g, "\n").split("\n") paragraphs = 连续非空行合并为一段 → 记 [lineStart, lineEnd](空行视为段界) // ② 指标类(句式偏离 / 抽象度过高 / 留白异常)—— 按「段落是否同样出带」归因 for 每个章级出带的维度 v: if |v.value − v.mu| < 量级门槛 → 忽略该维度(不进清单) for 每个段落 p: (p.text.length ≥ 40 字才参与归因) if p 与全章「同向」偏离 且 judgeAgainstBaseline(测量(p)) 该维也出带: 收下 { p, 本段值, |σ| } 按 |σ| 降序取前 1 段(MAX_PARAGRAPH_ITEMS_PER_METRIC = 1) ← v5.1.0:同一维度只报最严重的这 1 段,其余同类段落不再逐段列出, 只在 summary 的计数里体现(旧版该常量为 3) 一段都没有 → 退化成一条「全章层面」项:lineStart = lineEnd = 0,severity 降一档、effort = 3 有段落项 → lineStart/lineEnd = 该段行区间,excerpt = 该段原文截 80 字,effort = 2 // ③ 字面类(禁用词 / 语用不符)—— 逐行扫描全部命中,行号给到「区间列表」(v6.5.0) + locateWord(rawLines, word): + linesContaining(rawLines, word) ← core.js:全部命中行(文件内真实行号) + for 每一处命中: if isGuardedArtifact(word, line, at) → 记 guarded(丢弃该处) + else → 记 kept(该行进结果) + 全部命中都被守卫挡掉 → 该词不产生待办(挡掉的词在 summary 里披露) + ranges = mergeLineRanges(keptLines) ← core.js + rangesText = formatLineRanges(ranges) ← 「第2-10行,第52行」 + locate.lineStart = 首个区间起点,lineEnd = 末个区间终点(全部命中的超集) + locate.excerpt = 首个命中行的原文 trim 后截 80 字 + current = 「命中「词」K 处(位置:第2-10行,第52行)」;guarded > 0 时追加「另有 G 处疑似相邻巧合(邻接守卫忽略)」 + 档位:词长 ≥ 3 → hint 前缀【硬命中·3字以上】;词长 = 2 → 【待判·2字词】+「若读该行发现只是相邻巧合,可跳过本项」 + 仪式禁式(正则)→ 扫出全部命中,按 mergeLineRanges 汇总区间(上限 200 处),sample 取首个命中文本 + 称谓(XX小姐)→ 仍只报第一处(与 novel_continuity_check 同口径,prompt 全章统一) // ④ 情感过直 —— 段级命中计数;⑤ 衔接缺失 —— 用候选详情里「…」引号内的词做精确行定位; // 引号内取不到词 → 0/0 并在 hint 里说明「没有可精确定位的行」 // ⑥ 伏笔未回收 —— 跨章问题,正文里没有「哪一行」可指,一律 0/0 定位不到时:lineStart = lineEnd = 0,并在 hint 里说明这是全章 / 跨章层面的问题
宁可粗,也不给错的行号。指标类给的是段落区间(约 40 字起)而不是某一行,且段落必须自身也出带才被归因——否则会出现「全章留白偏高,却指向一段根本不带省略号的文字」这种自相矛盾的定位。做出这个取舍是因为错误的行号比宽泛的区间更有害:模型会照着一个错位置去改,改坏的是本来没问题的句子。凡归因置信不足,一律退到整段区间;连段都定不到时给 0 并在 hint 里说明。调用方应对 lineStart = lineEnd = 0 做显式分支,不要把它当作「第 1 行」。字面类同此口径:locate 的两端是全部命中区间的超集(首段起点 → 末段终点),精确到每一段的位置在 current / hint 的「位置:」里——要逐段核对请读那里,不要把 lineStart-lineEnd 当成一段连续文本。
字面类的定位与审计共用同一套判据(v6.5.0)。禁用词 / 客套禁词走 core.linesContaining + mergeLineRanges + formatLineRanges + isGuardedArtifact,与 novel_continuity_check 的用语审计逐字同口径——审计说「这[里正]是」不是问题,改稿台就不许再报一条待办(否则用户得在两个工具之间对着猜)。守卫判的是命中处紧邻的前/后一个字是否落在实证碰撞表(lib/lexicons/guards.js)里,只收有实证的字:错填会吃掉真命中(静默漏报),所以宁可少填。某个词的全部命中都被守卫挡掉时,该词不产生待办,并在 summary 的备注里如实披露「N 个词的全部命中经邻接守卫判为相邻巧合」。

行号的坐标系是绝对行号(1 起,含空行与标题行),与 novel_read 的 formatRead 一致,可直接把 lineStart 交给 novel_read 的 offset 读回上下文。换行先归一为 \n(\r\n / \r 都算一个换行),因此「字符偏移 → 行号」与「行数组下标」永远在同一个坐标系里。

落盘与改稿闭环

路径 = <root>/.novel-writer/audits/fix-plan-<书名>-<归一化章键>.json 归一化章键 = sanitizeSegment(normalizeChapterKey(chapter)) ← 例:「1」「01」「第1章.md」都归一为「第01章」;与 novel_summary 的章号键同源 文件内容 = { tool: "novel_fix_plan", version: "<插件版本,取自 package.json>", book, chapter { file, number, title }, chapterKey, generatedAt, baselineScope, summary, notes[], detectedIds[], items: [ { …9 个字段, state: "open"|"done"|"skip", stateAt: ISO|null } ] } ← 注意:落盘文件只有 baselineScope(没有 baselineMode);返回体两个键都有 plan → 重新测量 + 原子写(先写 .tmp-<pid>-<时间戳> 再 rename,走在 withFileTx 文件事务内; 与上一次同 id 的项保留其 done/skip 状态与 stateAt) verify → 读该文件 + 对当前正文重算,逐项给出三态(本次不写盘) mark → 读-改-写该文件,只把 itemId 指定项的 state / stateAt 更新,并刷新 updatedAt

必然需要落盘:mark 与 verify 要跨调用记得「这一项处理过了」。文件按书名 + 归一化章键命名,且章键取自解析后的章文件(chapter="1" / "第01章.md" / "码头等船" 三种写法都归到同一份),避免同章不同写法各写一份、verify / mark 换个写法就「找不到清单」。落盘与 novel_continuity_check 的审计报告同处 audits/ 目录,文件名前缀 fix-plan- 与审计报告区分。plan 每次重新测量并整份覆盖该章的清单,但会按 id 把上一次的人工标记搬过来:N 项此前已人工标记 done/skip(状态已保留)会写进 summary。正文改动后 id 会随位置变化,旧 id 的标记自然失效(不会张冠李戴)。

落盘失败(目录只读 / 磁盘满 / 文件被占用)不抛错,本次清单仍内存返回;summary 明确追加保存失败原因,planFile 为空,调用方不会被误导为已保存。

verify 的三态

status含义detail 的形态
resolved(已解决)该条目在当前正文中已检不出——原来的问题项在本次重算的清单里既没有同 id 命中,也没有「同 type + 稳定问题身份」或「同 type」的兜底配对。可能是真改好了,也可能是这一段文本被改动后换了形态。「当前正文中已检不出该问题(已修复或该处文本已改动)」;若该条曾被人工 done / skip,尾部追加「(人工标记 done / skip)」
pending(仍待处理)问题还在。分两种命中路径:① id 精确命中(正文该处未动);② 同 type 兜底——行号或数值变了但同类问题仍存在(先找 current 文本也相同的,找不到再放宽到只要同 type),仍判 pending,并在 detail 里说明位置变化。①「问题仍存在:<current>」;②「问题仍存在(位置/数值已变化 → 现为第 N 行:<current>)」。人工标记不改变判定,只在尾部追加「(人工标记 done / skip)」——避免「标了完成但问题还在」被静默吞掉
new(本次新检出)本次重算清单里存在、但上一次落盘清单里没有配对上的条目——改稿最常见的副作用,必须显式报出来,否则「越改越偏」不会被发现。「本次新检出:<current>」

checked[] 的元素为 { id, type, status, detail },其中 id 对 resolved / pending 保留旧清单的 id,对 new 是本次新生成的 id。summary 汇总三态计数(复测结果:已解决 X 项 / 仍待处理 Y 项 / 新检出 Z 项。)。清单文件不存在时不报错:所有本次检出的项都记为 new,summary 给出「未找到已落盘的改稿清单(<路径>)——本次按当前正文现算:…请先调用 buildFixPlan 生成清单。」

resolved 不等于「已回到带内」。判定口径是「这条待办在当前正文里还找不找得到」,而不是「该维度是否已落回基线带」。同一处改动可能让 A 项 resolved 的同时让 B 项变成 new;要确认整章是否回到带内,仍应跑一次 novel_style_check。

不误报

正常文本不产出待办项:各类型只在指标实际出带、命中禁用词 / 语用禁式或钩子确实未承接时才生成条目;全部合格时 items 为空数组,这是一次成功调用而不是失败。为了不误报,实现里设了五道闸:① 指标须同时越过相对容差带与绝对量级门槛;② 段落必须与全章同向偏离且自己也出带才被归因(同维最多 1 段,v5.1.0 起由 3 收紧到 1——同一维度只报最严重的那一段,其余同类段落由 summary 的计数体现,清单不再被同一维度的多段刷屏);③ 无基线、无伏笔、无法判定章号时整类跳过;禁用词表为空(当前文化基准下无可扫的词)时同样跳过该类并说明原因,不另行兜底;④ 章节正文短于 40 字时跳过全部指标类检查(空文本的六维全是 0,与基线一比必然「处处出带」);⑤(v6.5.0) 字面类命中先过邻接守卫(子串巧合不计),被守卫整词挡掉的词不产出待办并在 summary 里披露;已被本书用词豁免(allowWords)静音的词同样不产出。

清单长度有上限:单次最多 12 条(DEFAULT_MAX_ITEMS,v5.1.0 起由 40 收紧到 12),超出部分按排序截断,并在 summary 的备注里披露「待办超过 12 条,已按严重度从高到低截断 N 条(清单上限固定,先处理保留下来的高优先级项)」。上限收紧的用意是让清单可执行:一次抛出四十条,模型只会从上往下挑几条做,其余全部沦为空转;只留 12 条则每一条都是排序意义上最值得先动的,被截断的部分不是「漏掉了」,而是「这一轮先不管」——先按清单顺序处理保留下来的项,改完再跑一次 plan,原先被截断的项若仍成立会重新排进来(同一维度现在只报最严重的 1 段,因此回访时的清单也不会重新膨胀)。若清单接近或撞到这个上限,说明这一章的偏离是系统性的(通常该整段重写,而不是逐项修)。注意上限不由调用方传参放宽:buildFixPlan 内部虽有 opts.maxItems,但工具层固定传 {},提示里不会再出现「可传 opts.maxItems」这类做不到的建议。

词表与语用来源(与开写包 / 审计同源)

改稿台的禁用词与语用规范不再只读用户登记的字段,而是与开写包、novel_continuity_check 共用同一个裁决点 core.resolveStyleRule(settings, chapterTexts):

  • 禁用词 = 去重(用户登记的 bannedWords ∪ 该文化基准的默认表)。文化基准取 worldview 登记(cultureSource: "worldview");未登记时对本章正文跑 hintCulture 检测,判不出则落欧式默认表。旧行为:用户只登记了 basis 而未填 bannedWords 时,改稿台会写「未登记 worldview 禁用词表,禁用词检查已跳过」,一条禁词都不报;新行为:按文化基准合并默认表后逐词扫描。
  • 语用规范取合并结果:speechStyle 不再只取「单个条目」,而是合并全部 worldview 条目(数组去重、对象浅合并),与 novel_continuity_check 同口径。
  • 空词表的提示文案跟随实际基准:不再一律写「未登记 worldview 禁用词表」,改为「当前文化基准「X」的禁用词表为空,禁用词检查已跳过」。空表可能来自两种情况:显式登记 bannedWords: [],或「现代都市」基准本身没有文化禁用词——两者都会如实写明基准名称,便于判断。
  • 显式 bannedWords: [] 的语义:表示「我不要默认表」,此时确实不扫禁词(沿用既有语义),只用用户自己登记的表(为空则该类跳过)。
  • 逐词静音(v6.5.0):worldview.allowWords = 本书用词豁免。登记后这些词在本书的禁用词与 speechStyle.honorBad 两路都不再报(过滤发生在同一个裁决点,所以改稿台 / 开写包 / 审计三处自动一致)。只作用于本书——不提供全局静音:全局静音会静默削弱所有书的覆盖,而人很难记得自己什么时候加过。用途是那些「在这本书里本来就是对」的词:修仙小说里的「修士」、中文历史小说里的「教堂」。登记方式(走真实工具入口,action: "add" 即可,不必先有别的 worldview 条目):
    novel_settings add category=worldview name="用词豁免" allowWords=["修士"] → 写入 worldview 条目的 allowWords(净化:数组、去空、去重、每项 String、单项限长 40 字、最多 200 条) → core.resolveStyleRule 把这些词从 bannedWords 与 speechStyle.honorBad 里滤掉,并在 sources.muted 计数
    被静音的词不是被删掉:它仍留在词表里,别的书照常检查;本次清单的 summary 备注会写明「已按本书用词豁免静音 N 个词」——少报必须留痕,否则与漏报无法区分。

约束与边界

  • 不生成正文:hint 只给改写方向,anchor 只给可参照的原著样例;返回体里没有任何「改写后的句子」。插件不调用在线模型,也从不代替作者写字。
  • 只读正文,只写自己的清单文件:plan 不修改章节正文,落盘只发生在 <root>/.novel-writer/audits/ 下的 fix-plan-*.json;verify 连清单都不写,也不写指标缓存或旧版伏笔迁移文件;mark 只改该文件里的处置状态。
  • 取消会抛错,不会伪装成空结果:调用被中止时(AbortError)三个动作一律向上抛,不落进任何降级分支——否则一次取消会返回「成功但清单为空」,被误读成「这一章没有问题」。
  • 行号可以被整段,但不会给错:指标类 locate 的最小粒度是段落;字面类(禁用词 / 语用)给的是命中行的区间列表(current / hint 里的「位置:第2-10行,第52行」逐段精确,locate 两端是这些区间的超集);lineStart = lineEnd = 0 表示全章 / 跨章层面,需按分支处理。行号是绝对行号(与 novel_read 同口径),不随任何 offset 重编号。字面类还有一个前置过滤:命中处紧邻前/后一个字落在实证碰撞表里(如「这[里正]是」「[在下]面」)时该处不计入,全被挡掉的词不产出待办。
  • 档位只在 hint 前缀里,不改 type:type 是十一类固定中文词(e2e 契约),档位由 hint 的 【硬命中·3字以上】 / 【待判·2字词】 表达。待判档必须留「可以不改」的出口——2 字中文词当子串的概率天然很高(实测无辜句子里 14/15 条是误报),一律照改会把作者的正常用词改掉。
  • id 与正文位置绑定:先按 id 精确配对;位置变化后,仅按可确认的问题身份(禁用词、语用命中词、指标方向与摘录等)配对。只同类型不能确认是同一项时分别报告 resolved 与 new。
  • mark 的校验只有三条:itemId 非空、state ∈ {done, skip}、该章已落盘清单里存在该 id。任一不满足即报错;它不重新测量、不校验正文是否真的改了,标记纯粹是给下一次 plan / verify 看的处置台账。
  • 依赖已有材料,质量随材料走:禁用词 / 语用项依赖 novel_settings 的设定表,衔接项依赖上一章可读,伏笔项依赖 novel_plot 的登记质量。材料缺失时相应类别不产出待办(而不是产出猜测),原因写进 notes 与 summary,并随清单一起落盘。
  • 六维相关项可能整体缺失:需要至少 2 个基线章节(按全书 时即全书除本章外至少 2 章、全书合计至少 3 章;按部分 时即实际生效的基线章至少 2 章)才能估计作者自身波动;本章正文短于 40 字也跳过。此时句式偏离 / 抽象度过高 / 留白异常三类不产出,原因写进备注;style_check 的同一限制在此同样成立。
  • 首次调用需要测量本章:复用分章测量缓存(metricChaptersCached),已预热时成本很低;正文刚改过则要重算本章。
  • 优先级算法属经验值:severity 的分档阈值与 effort 的分级是规则给定值,不是从真实稿件学出来的;清单顺序应按实际改稿手感校准,而不是当作绝对权威。
  • 工具开关与引擎门禁是两道不同的闸:三个动作都要过 assertToolEnabled("novel_fix_plan")(工具级开关关闭时直接报错,可用 novel_sentence_config 重新开启);此外 plan / verify 还要过「写作助手功能」总开关(关闭时拒绝执行,文案引导去侧边栏开启),mark 不受第二道闸约束。

相关工具

  • novel_style_check —— 同一次测量的「数据视图」:相似度、偏差清单、六维出带判定与 fixAnchors。本工具是它的「行动视图」。
  • novel_continuity_check —— 跨章审计(衔接 / OOC / 大纲走偏),其衔接检测与本工具的「衔接缺失」同类;审计报告与本工具的清单同处 audits/。
  • novel_chapter_brief —— 写之前的材料装配(承接口、伏笔距离、基线、锚段),与本工具构成「写前 / 改后」两端。
  • novel_settings —— 禁用词与语用规范的数据源;要先补登记再跑本工具,否则禁用词 / 语用两类不会产出。
  • novel_read —— 按 locate.lineStart 读回该段上下文,核对是否真是要改的地方。

源码位置

  • lib/vector-audit.js —— v7.0.0 Phase 2 的向量审计(detectSemanticLoop / checkOutlineFulfillment):只在改稿台调用,受 features.semanticEmbedding 开关与本地引擎可用性双重门控;块文本按哈希做内存 LRU,verify 时复用;判定用章内 z 分数 + 词汇新颖度,不生成任何正文句子。
  • lib/fixplan.js —— 改稿台主体:buildFixPlan / verifyFixPlan / markFixItem 三个导出,以及内部的 computeFixPlan(测量 → 六类收集 → 排序截断 → 落盘)、makeItem / makeItemId(9 字段与稳定 id)、buildParagraphIndex / lineOfOffset / firstLineContaining / lineExcerpt(段落索引与行号归因)、locateWord / locateOfRanges / hitClause / TIER_TAG / reviewTierTail(v6.5.0:字面类的守卫过滤 + 行号区间 + 档位话术)、sortItems(四级排序)、METRIC_TYPE / TYPE_SLUG / METRIC_DIRECTION / METRIC_MIN_DELTA(类型映射与阈值常量)、collectMetricItems / collectBannedItems / collectPragmaticItems / collectEmotionItems / collectBridgeItems / collectPlotItems(前六类判定);v7.0.0 的「AI套话」在主流程 ⑤b 内联完成(scanSlop → 按类别归并 → makeItem)。
  • lib/lexicons/slop.js —— v7.0.0 套话词表(8 类 / 73 条 pattern,SLOP_CATEGORIES 与 scanSlop):只收高确定性、低误报的结构片段,命中给行号 + 类别 + 一句改法;正则安全纪律由 test/slop-regex-safety-test.mjs 逐条门禁(禁嵌套量词、禁 .* 跨句、{n,m} 的 m ≤ 12)。
  • lib/lexicons/guards.js —— 子串碰撞守卫表(COLLISION_GUARDS 的 notBefore / notAfter / notAfterSeq / notBeforeSeq + 实证 evidence);改稿台与审计共用 core.isGuardedArtifact 判定,口径不可能分叉。
  • lib/index.js —— registerNovelFixPlan:工具注册、参数与输出 schema、三个动作分支、mark 的 itemId + state 校验(在引擎门禁之前分支)、plan / verify 的「写作助手功能」门禁、渲染文本。
  • lib/core.js —— resolveStyleRule(禁用词 / 语用词源的单点裁决,开写包 / 审计 / 改稿台共用;allowWords 逐词静音也在这里生效)、linesContaining / mergeLineRanges / formatLineRanges / isGuardedArtifact(v6.5.0:字面类定位与子串守卫,本页与审计共用)、withFileTx / atomicWriteJson(清单文件的原子写与串行事务)、normalizeChapterKey / sanitizeSegment(落盘文件名的章号归一与清洗)、readSettings(禁用词与语用规范)、readPlots(伏笔表)、detectChapterBridge(衔接检测)、metricChaptersCached / buildStyleAnchorPackage(分章测量缓存与锚包)、readSentenceState(引擎开关)。
  • lib/style-metrics.js —— measureStyleMetrics / computeBaselineFromPerChapter / judgeAgainstBaseline / METRIC_ORDER / METRIC_LABELS(六维测量、基线与出带判定)。
  • lib/core.js —— ALL_TOOLS / TOOL_LABELS 注册表条目(开关面板与关闭报错文案)。