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

novel_plot

伏笔 / 剧情钩子登记表。以「一部作品一个 JSON 文件」维护伏笔条目,用 status 区分 open(待回收)与 done(已回收);scan 动作把伏笔内容切成关键词后遍历章节正文,自动在条目上标注「在哪几章被提及」;action: "graph"结构视图,对全书做跨章计算——伏笔生命周期、人物出场矩阵与连续缺席、线索活跃度、时间线登记顺序、大纲与正文的对照。纯本地文本统计与包含判定,不做语义推理,也不调用在线模型。

为什么要有结构视图。伏笔表、人物表、时间线在这之前都是离散静态表:能查、能改,但没有任何跨章计算。而长篇崩掉通常不是因为某一章文笔差,而是线断了、人丢了、伏笔忘了——这三件事都只有跨章才看得出来。结构视图不新增数据,只把已有登记表按章号轴算一遍距离与空档。它并入本工具而不新建工具,是因为伏笔生命周期本就属于 novel_plot 的领域,且 action 已是枚举,加分支向后兼容。

参数

参数类型必填说明
bookstring书名,即 novels/ 下的子目录名。经 sanitizeSegment 过滤。
actionstring枚举七值,见下表。非枚举取值与省略一律按 list 处理。
idstringupdate / done / delete 必需:目标伏笔 id,必须已存在。
contentstringadd 必需;update 可选:伏笔内容描述。
chapterstring伏笔出现的章节。scan 时用作「只扫该章」的章节标识。
notestring备注(如何回收 / 何时回收)。
typestring枚举:剧情 / 设定 / 道具 / 人物 / 其他。非枚举值被静默丢弃。
prioritystring枚举:high / medium / low。非枚举值被静默丢弃。
relatedCharactersstring[]关联人物。add 写入;update 整组替换。
locationsstring[]关联地点。add 写入;update 整组替换。
payoffConditionstring回收条件。
absenceThresholdintegergraph 使用:人物连续缺席多少章才算一条 absences(默认 5)。只接受正整数,其它值按默认处理。其余动作忽略该参数。
rootstring章节库根目录。缺省时取插件配置的 root,再缺省取会话工作目录。

action 取值

取值语义前置要求返回体形态
list列出全部伏笔(默认值)。全表
add登记新伏笔,status 固定为 opencontent单条
update修改指定条目的文本字段。id 且存在单条
done标记已回收(status → done)。id 且存在全表
delete删除条目。id 且存在全表
scan扫描章节正文,更新 open 条目的提及章节。作品目录下至少一个章节文件全表 + 统计文案
graph结构视图:对全书做跨章计算,输出伏笔生命周期、人物出场矩阵、线索活跃度、时间线顺序与大纲对照。无(材料缺失时按字段降级)五项计算结果 + 汇总 + 降级说明

输出结构

契约字段(output.schema.required):bookactionentries 恒返;message 按动作出现。

字段类型内容
bookstring净化后的书名
actionstring归一后的动作(可能与传入值不同)
entriesobject[]伏笔条目数组,见下。graph 分支恒返回空数组 []——它不为放松共享 output.schemarequired: ["book","action","entries"] 而改动契约(放松会让另外 6 个动作少一层断言),因此用一个空数组占位;结构视图的数据全部在 plotLifecycle 等专用字段里。
messagestring操作结果文案,例如「已登记伏笔 #m1x2k9ab3f」「扫描 12 章,更新 3 条伏笔的提及记录」。graph 不返回该字段(改用 summary)。
totalChapters / plotLifecycle / characterMatrix / threadActivity / timelineOrder / planVsActual / summary / degradedgraph 返回:五项跨章计算结果与章数、概览文案、降级说明,见下文「结构视图」。

entries 的元素契约(required: ["id","content","status"]):

字段类型来源
idstring插件生成,调用方不可指定
contentstringcontent 参数
statusstring枚举 open / done,归一后写入
chapter / note / payoffConditionstring同名参数
type / prioritystring同名参数,仅在命中枚举时写入
relatedCharacters / locationsstring[]同名参数
mentionedInstring[]scan 写入,元素是章节文件名
lastMentionedstringscan 写入,最近一次命中的章节文件名
createdAt / updatedAtstringISO 8601 时间戳
两种章节字段口径不同。chapter 是调用方传入的原文(如 第03章 或「第三章」),mentionedIn / lastMentioned 由扫描器写入,元素固定为磁盘文件名(如 第03章.md)。调用方做跨字段比对时不应假定二者同构。

数据模型与落盘

{
  "book": "雨夜灯",
  "entries": [
    {
      "id": "m1x2k9ab3f",
      "content": "母亲留下的半枚铜钱",
      "chapter": "第03章",
      "note": "与第19章当铺掌柜的银票核对",
      "type": "道具",
      "priority": "high",
      "relatedCharacters": ["沈砚"],
      "locations": ["西市当铺"],
      "payoffCondition": "主角认出铜钱缺口与银票吻合",
      "status": "open",
      "mentionedIn": ["第03章.md", "第07章.md"],
      "lastMentioned": "第07章.md",
      "createdAt": "2026-09-13T02:11:07.412Z",
      "updatedAt": "2026-09-13T02:40:55.008Z"
    }
  ]
}

上例的 id 与时间戳为示意值,字段名与结构取自 writePlotsnormalizePlotEntry 的实际读写口径。

落盘位置与迁移

  • 当前路径:<root>/.novel-writer/plots/<book>.json,内容为 { book, entries },以 2 空格缩进序列化。
  • 旧路径:<root>/.novel-writer/<book>.jsonreadPlots 在新路径不存在时回读旧路径,命中且含 entries 数组则复制到新路径;旧文件保留不删。
  • 读不到文件或 JSON 损坏时返回空数组,不抛错——首次登记即视为「空表」。

读写流程与事务语义

  • 所有动作先经 withFileTx(plotsFile(root, book), …) 取得该文件的独占票:同一路径的读-改-写串行执行,不同书(不同路径)互不阻塞。
  • list 是只读分支,不写盘;其余动作在变更后调用 writePlots,并把书库根记入全局状态(writeSentenceState({ lastRoot }))。
  • writePlots 内部走 atomicWriteJson:先写 <文件>.tmp-<pid>-<时间戳>renamerename 失败回退整文件直写;两者都失败抛出带 code = "ENOVELWRITEFAIL" 的错误,不再静默假报成功。
  • 返回前对全表做一次 normalizePlotEntry 清洗与排序:status 相同的按 createdAt 字典序升序,open 恒排在 done 之前。

id 生成与状态机

id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6)

时间基部分保证趋势有序,随机后缀 4 位用于降低同毫秒碰撞概率:add 一次只生成一个 id,不查重。

状态是单向的:add 固定写 opendone 固定写 doneupdate 不接受 status 参数。没有任何动作可以把 done 改回 open——需要「重新打开」时只能 delete 后重新 add(id 与 createdAt 都会变)。读取时脏数据兜底为 openstatus !== "done" 的一切取值(含 in-progress、缺失)都归一为 open

scan 的提及扫描算法

扫描分两步:先把每条 open 伏笔的 content 转成关键词集合(进入章节循环前只算一次),再逐章做纯子串包含判定。

关键词提取 plotKeywords

  1. 剥掉全部非汉字字符:content.replace(/[^\u4e00-\u9fff]/g, "")。标点、数字、拉丁字母都不参与。
  2. 按长度 4 → 3 → 2 滑窗枚举子串。
  3. 子串中非停用字的占比须 ≥ ceil(长度 × 0.6)(长度 2 需 2 个非停用字、长度 3 需 2 个、长度 4 需 3 个)。停用字表含「的了是在我有和就都不一一个这那与及或但是因为所以如果然后而且比如什么怎么自己她们他们你们我们咱们没有不是别莫未」。
  4. 去重后按长度降序取前 8 个

命中判定

命中 = keywords.length > 0 且 keywords.some(k => chapterText.includes(k))

任一关键词在章节正文中出现即算该章提及:把 chapter.file 追加进 mentionedIn(已在数组内则跳过),同时刷新 lastMentionedupdatedAt。计数器只在「本条目本不存在于该章」时递增,因此返回文案里的更新条数反映的是新增的提及,而不是命中总数。

遍历范围有限定:只处理 status === "open" 的条目——已回收的伏笔不再更新提及记录;chapter 参数存在时只扫该章(先用 findChapter 解析,找不到则报错),否则扫全部章节。

结构视图 action: "graph"

纯读、纯计算的跨章视图:除 book(与可选的 root / absenceThreshold)外不需要任何参数,不写任何文件(与 scan 不同——scan 会更新提及记录),也不复用会在旧位置迁移写盘的 readPlots。它把三张登记表(伏笔 / 设定 / 创作资料)放到同一条章号轴上,输出五项结果。

章号轴的口径:章号取自 scanChaptersnumber解析不出章号的文件按扫描顺序占位编号(i + 1参与计算,并在 degraded 里点名(否则它们会被「全书最大章号」口径整段吞掉)。totalChapters 是章节文件个数,而所有距离 / 空档计算的基准上界是 maxChapter= 现有章节里最大的章号——章号不连续(如缺第 5 章)时两者不相等,距离按章号之差而非文件序号之差计算。

唯一的抛错路径是作品目录不存在。缺伏笔表 / 缺人物表 / 缺时间线 / 缺大纲 / 空书 / 单章读失败一律返回空值 + degraded 说明。assembleGraph 内部若有异常数据或 IO 事故,也会兜底返回字段齐全的空结构并写清中文原因,保证调用方永远拿得到同一套字段。

1 · plotLifecycle 伏笔生命周期

只收 status === "open" 的伏笔(done 只进 summary 的条数,不进本数组) 埋设章号 firstChapter = min( 三处来源能解析出的章号 ): ① 全书正文关键词命中 ← plotKeywords(content) 在该章正文里做包含判定: ≥3 字长词命中 1 个即算;2 字短词需命中 ≥ 2 个 ② entry.mentionedIn[] ← scan 写入的章节文件名,逐个 parseChapterNumber ③ entry.chapter ← 登记时的章节字段 一处都没有 → firstChapter 省略、distance 省略(并在 degraded 点名) distance = maxChapter − firstChapter (maxChapter = 全书最大章号; maxChapter 为 0 时 distance 省略) 输出 = { id, content, priority, type, firstChapter, distance, status: "open" } priority / type 原样透传(priority 缺失时为空串,不做归一) 排序 = firstChapter 升序(未知排最后)→ id 字典序 ← 先看最早的线索

这是 novel_chapter_briefopenPlots[].distance全书版,但两者口径并不完全相同:开写包只从登记字段(chapter / mentionedIn / lastMentioned)推最早提及章号,本视图还会拿伏笔内容的关键词去全书正文里找首次命中章,因此「从未 scan 过」的伏笔在本视图里更可能算出 firstChapter。已回收条目不在本数组中,只以条数出现在 summary 里。

2 · characterMatrix 人物出场矩阵与连续缺席

characters = 设定表 characters 表里 name ≥ 2 字的名字(去重;alias 同表格里 <2 字的别名被忽略并记 degraded) rows[章] = { chapter: 章节文件名, number: 章号, present: [ 该章正文中出现名字或其任一别名的人物名 ] } absences[] = { name, from, to, length } 仅统计「首次出场之后」的区间(首次出现之前的章不算缺席) 连续缺席的章数 ≥ 阈值(默认 5,可用参数 absenceThreshold 覆盖)才输出 length = 该区间内连续缺席的章数(按章序计数) —— 章号不连续时 length 与 to − from 不相等 到书末仍未再出场时 to 取最后一章的章号 排序 = name 中文拼音序 → from 升序

只统计「首次出场之后」的区间:人物在第 20 章才登场,前 19 章当然没有他,那不是缺席。判定为纯字符串包含(含别名),不做语义推理——别名登记不全时会漏判出场,进而把正常出场误报为缺席,因此结果应视为候选。全书都未出场的人物不进 absences,而是单独产出一条 degraded 提示。

length 是「连续缺席的章数」,不是「章号之差」。这与 threadActivitylength / gap 口径不同——后两者算的是章号之差。同一条缺席区间 from: 8, to: 14 在章号连续时 length = 7;若中间缺了第 10 章,length 仍是实际缺席的章数(6),而 to − from = 6 恰好相等只是巧合,一般情形下二者不等。另外,设定表登记了 firstSeen 时,还会与正文实际首现章做一次交叉核对,不一致时产出一条以「注:」开头的 degraded 提示(与「缺数据」类降级原因区分开)。

3 · threadActivity 线索活跃度

聚类(三层启发式,并查集取传递闭包,按可靠性从高到低): ① 两条的 relatedCharacters / locations 有交集(用户显式登记,最可靠) ② 两条的内容关键词共享 ≥ 3 字的片段(plotKeywords 里长度 ≥ 3 的部分) ③ 某条的登记实体名出现在另一条的内容里(A 登记「林昭」、B 的内容提到「林昭」) 聚不出来 → 每条独立成线 每条线计算: 活跃章号 chapters = 簇内成员「被提及章号」的并集(每条的被提及章号 = 正文关键词命中 ∪ mentionedIn ∪ chapter 三处解析结果),升序去重 跨度 length = max(chapters) − min(chapters) → 只活跃 1 章时为 0 最大空档 gap = 相邻活跃章号之差的最大值 → 只活跃 1 章时为 0 线名 thread:优先「多条共享的人物/地点」(≥ 2 条出现的)取最多 2 个,其次簇内任意登记实体, 再次多条共享的内容关键词,最后退化为首条伏笔内容前 16 字 输出 = { thread, chapters, length, gap }(每簇一项) 排序 = 首次活跃章升序(无活跃线索的排最后)→ 线名中文拼音序

lengthgap 都是「章号之差」而不是「章数」:只活跃 1 章的线 length = 0gap = 0;活跃于第 1 / 2 / 3 / 7 章的线 gap = 4。跨度大而最大空档也大的线,通常就是「断了」的那条:人物还在、地点还在,但这条线上的伏笔连续十几章没有任何推进。plotIds 只在内部使用,不出现在返回体里。聚类只基于 open 伏笔;已回收的条数只进 summary

4 · timelineOrder 时间线登记顺序核对

遍历设定表 timeline 表(数组顺序 = 登记顺序),逐条产出: { day, event, chapter, number, issue } day = entry.day ?? entry.name event = entry.event ?? entry.description chapter = entry.chapter 转成字符串(缺失 = 空串) number = parseChapterNumber(chapter);解析不出 → 省略 issue = "" 或一条固定文案 判定(单趟扫描,维护 maxSoFar): number 省略的行 → 跳过,不参与判定,也不更新 maxSoFar number < maxSoFar → 该行 issue = "章号顺序与登记顺序不一致" 注意:命中后更新 maxSoFar(避免把「已经倒退过」当成新的基准) 否则 → maxSoFar = number

也就是说:issue 只标在「章号小于此前已登记过的最大章号」的那些行上——即登记顺序与章号升序不一致;章号相同的并列不算不一致,章号解析不出的行既不判错也不影响后续基准。时间线表是手工维护的,这种不一致通常意味着「补登记时插在了中间」或「章号写错了」。本项只指出不一致,不自动重排表——重排会掩盖登记时的真实意图。

5 · planVsActual 大纲方向行对照

读 novels/创作资料/<书名>/剧情大纲.md,只取形如「- 12 本章方向…」的方向行: 行正则 ^[-*]\s+\d{1,4}\s+\S → number = 行首数字,direction = 去掉「- N」之后的正文 关键词提取(方向行,最多 12 个): 主路径 core.extractKeywords(direction, 12) 取 .word ← 注意 extractKeywords 只保留「出现次数 > 1」的词,十几字的方向行 几乎必然产出空集,所以实现里有明确的退化分支 主路径产出 < 2 个 → 退化为「方向行的相邻二字片段」 按非汉字切分成 CJK 连续段,每段取全部相邻二字片段, 丢掉「两个字都是停用字」的片段(CJK_STOP_CHARS),与 novel_continuity_check 同口径 两者并集去重后截断到 12 个 逐条方向行计算: number 找不到对应章节文件(章节 number 逐一对齐)→ unwritten += 1, 该行不进 rows(大纲通常会预先列出尚未动笔的章节) keywords 为空 → noKeywords += 1,rows 补一条 { number, direction, overlap: 0, keywords: [] } 否则 hit = keywords.filter(k => 该章正文.includes(k)) ← 对正文做子串包含判定 rows.push({ number, direction, overlap: 四舍五入到 3 位小数(hit.length / keywords.length), keywords: hit }) ← keywords 放的是命中的子集,不是全部方向行关键词

输出的是一行一条方向行的记录,不是一章一条:没有正文的章节不产生记录,只累加 unwritten 与一条 degraded 说明;关键词为空的记 overlap: 0 并单独计数。返回体里没有「全部方向行关键词」这一项——keywords 字段就是命中列表,因此它同时也说明了「命中了哪几个词」。这是 novel_continuity_check 大纲走偏模式的全书版:彼处一次只比对一章,此处一次列出全书各章的重合率趋势,便于发现「连续若干章都在偏离大纲」这类系统性走偏。

汇总与降级

字段类型内容
totalChaptersinteger章节文件个数(scanChapters 的结果长度)。它是章数,不是章号上界——距离 / 空档计算的基准是「全书最大可解析章号」。
summarystring一句话汇总,固定按此顺序拼接:全书 N 章;未回收伏笔 X 条(已回收 Y 条);人物 Z 名,连续缺席区间 W 处;剧情线 V 条,最大空档 G 章;时间线 T 条(若有逆序则补「(K 条章号顺序与登记顺序不一致)」);大纲方向 K 条,平均重合率 P%(若有 overlap < 0.2 的章则补「(L 章低于 20%,可能偏离方向)」)或「大纲方向未计算」;若 degraded 非空则补「降级 D 项(见 degraded)」。最后追加一段【风险】:空档最大的线、缺席最长的人物、埋得最久的伏笔(三者各取第一)。
degradedarray取不到的材料与原因,元素是字符串(不是对象)。空数组表示材料完整。除「少了什么数据」外,人物首现章与登记不符这类提示也以「注:」开头出现在这里。

无伏笔表 / 无人物表 / 无时间线 / 无大纲 / 全书只有 1 章等情形,一律返回空值 + degraded 说明,绝不抛错:跨章视图在资料不全时仍然可用(能算的那几项照算),只是结果不完整。

graph 与其它动作的差别。list / add / update / done / delete 面向单条伏笔的增删改查,scan 面向提及记录的回填(有副作用),graph 面向全书的跨章关系(只读)。四类动作的输出形状互不相同,调用方应以返回体里的 action 字段判断实际执行的是哪一个。

约束与边界

  • book 必填,经 sanitizeSegment:剥除 \ / : * ? " < > |、首尾点与空白;命中 Windows 保留名(con / nul / com1…)时前置下划线;清洗后为空则报错终止。
  • action 非枚举值静默退化为 list:传入 action:"close" 不会报错,而是返回全表。调用方必须以返回体里的 action 字段判断实际执行了哪个动作。
  • type / priority 的非枚举值是静默丢弃,不报错也不写字段;add 时该键为 undefined,序列化后不出现。
  • id 必须已存在update / done / delete 找不到目标时抛「伏笔 #xxx 不存在」,整次调用失败且不写盘。
  • 纯符号或纯英文的伏笔永远扫不到plotKeywords 只保留汉字,content 若无汉字则关键词为空数组,任何章节都不会被标注为提及。
  • scan 需要章节文件:作品目录不存在或没有符合 .md / .markdown / .txt 的章节时抛错;空作品不会返回「0 条更新」。
  • mentionedIn 只增不减、无上限:条目在 open 期间每被新章命中就追加一项,全书扫一遍后可能积累上百个章节名;它不参与任何截断,也不随正文删改而回收。
  • 并发语义是「整表读-改-写 + 按文件串行」:同一部作品的并发 add 会依次串行,不会互相覆盖;但这也意味着跨进程(插件进程 + 另一个直接改 JSON 的脚本)没有锁保护,手改文件的并行写入可能被整表覆盖。
  • 返回体形态依动作而异add / update 只返回被操作的那条;list / done / delete / scan 返回全表。想在 done 后只取单条,需要在返回的 entries 里按 id 自行筛选。
  • 跨版本口径:v0.8.0 起数据文件由 .novel-writer/<book>.json 迁到 plots/ 子目录(读取时自动迁移);v3.5.0 起非 donestatus 统一归一为 open;v4.0.0 起 scan 的关键词集合改为进入章节循环前预计算,并对 null / 标量脏条目做守卫(旧版会在坏数据上抛 TypeError 打挂整个工具)。跨版本对同一份数据的扫描耗时与容错表现不同,提及结果本身的口径未变。
  • graph 是只读分支:不更新提及记录、不写盘、不改表,也不经过 withFileTx 写事务与 readPlots(后者在「新位置缺失、旧位置存在」时会迁移写盘);只有 scan 会写(更新 mentionedIn / lastMentioned),其余增量动作走 writePlots。想在不产生副作用的前提下看清全书结构,用 graph
  • graph 依赖登记质量,但埋设章不只看登记plotLifecycle.firstChapter 取「全书正文关键词命中 ∪ mentionedInchapter」三者的最小值——从未 scan 过、也没写 chapter 的伏笔,只要内容关键词在正文里出现,仍能算出埋设章。三处都推不出时才把 firstChapter / distance 整键省略并记 degraded。人物出场靠纯字符串包含判定,别名未登记会漏判;时间线核对只认登记顺序,不推断意图。
  • graph 的缺席阈值是「连续 ≥ 5 章」:低于 5 章视为正常轮换,不列为缺席;阈值可用参数 absenceThreshold 改(只接受正整数);且只统计首次出场之后的区间——人物登场之前的章节不计入缺席。absences[].length连续缺席的章数,与 threadActivity 的「章号之差」口径不同。
  • 章号轴包含「解析不出章号」的章节:这类文件按扫描顺序占位编号(i + 1)参与全部跨章计算,并记一条 degraded(占位编号可能与真实章号冲突)。所有距离 / 空档的基准是全书最大可解析章号 maxChapter,不是 totalChapters;章号不连续时两者不等。
  • MCP 形态下的 root 限制:工具参数里的 root 必须落在服务器启动参数 --root 指定的书库根之内(含根本身),越界会被静默回退为书库根,同时记 stderr 日志。
  • 参数表禁止额外字段parameters.additionalProperties = false,未声明的参数名不在契约内。新增的 graph 分支要求 output.schema 同步声明其返回字段(schema 为 additionalProperties: false,漏声明会让宿主直接拒收返回体)。

相关工具

  • novel_settings —— 人物 / 地点 / 道具 / 时间线 / 世界观用语规范五张设定表,伏笔里的关联人物与地点通常先在此登记;graph 的人物出场矩阵与时间线核对直接读这三张表。
  • novel_outline —— 创作资料(含「钩子记录.md」、「剧情大纲.md」):逐章结尾钩子的行级回填与大纲方向行,与伏笔表是两套不同的追踪对象;graphplanVsActual 读的是大纲方向行。
  • novel_continuity_check —— 对照设定表扫描全书输出矛盾候选;世界观禁用词与语用规范检查在彼处实现,大纲走偏的单章版亦在彼处。
  • novel_chapter_brief —— 动笔前的材料装配,其 openPlots[].distancegraphplotLifecycle.distance同一族口径(都是「全书最大章号 − 最早提及章号」),但开写包只从登记字段推最早提及章号,本视图还会拿关键词去正文里找首次命中章,因此两者可能给出不同的 firstChapter
  • novel_summary —— 章节摘要,用于回忆前情而非追踪伏笔。

源码位置

  • lib/graph.js —— 结构视图主体:buildStoryGraph(唯一对外导出)与内部的 assembleGraph(主流程:章节 → 设定表 → 摘要 → 伏笔 → 五项计算)、buildPlotLifecycle / plotKeywordSets / textMentionsPlot(伏笔埋设章)、collectCharacters / characterPresent / computeAbsences(人物矩阵与连续缺席)、clusterPlots / threadLabel / buildThreadActivity(剧情线聚类与活跃度)、buildTimelineOrder(时间线顺序核对)、directionKeywords / pushBigrams / buildPlanVsActual(方向行关键词与重合率)、buildSummary / emptyGraph(概览文案与空结构兜底)。
  • lib/index.js —— registerNovelPlot:工具注册、action 枚举(含 graph)、absenceThreshold 参数、输出 schema(含五项结果字段)、graph只读早返回分支({ ...graph, entries: [], action: "graph" },在 withFileTx 之外)与渲染文本。
  • lib/core.js —— plotsFile / legacyPlotsFile / readPlots / writePlots / plotKeywords / normalizePlotEntry / normalizeSettingEntry(伏笔与设定表的读写口径);withFileTxatomicWriteJsonsanitizeSegmentscanChapters / findChapterreadSettingsreadSummariescreationDir / CREATION_FILES(大纲方向行的读取路径)、extractKeywords / CJK_STOP_CHARS(方向行关键词的主路径与停用字集)。
  • mcp/server.mjs —— buildArgs / isInsideRoot:root 越界回退与参数注入。