novel_chapters
列出某部作品的全部章节:章号、标题、字数、行数与更新时间。工具按扩展名过滤目录下的文件,对每个文件名做章号解析(阿拉伯数字、中文章号、全角数字)与标题清洗,再逐章 stat + 读全文得到统计量。book 为必填项;解析不出章号的文件仍会列出,但 number 字段缺省。
⚠️ 同章号多文件 N 组,逐组列出章号与撞号文件名。这不是去重、也不修改任何文件,只把「两条看起来一样的条目其实是两份文件」这件事显式说出来——因为按章号定位的工具此时会拒绝执行(见 novel_read)。返回体的 chapters 数组仍如实包含全部记录。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
book | string | 是 | 书名,即 novels/ 下的子目录名。空串或纯空白会被拒绝;值经 sanitizeSegment 清洗后使用。 |
root | string | 否 | 章节库根目录。省略时按 config.root → 会话工作目录回退。 |
schema 声明 additionalProperties: false。没有分页、偏移或数量限制参数:一次调用返回该作品的全部章节。
输出结构
契约字段(output.schema):顶层 book 与 chapters 为必返;每个章节元素必须含 file、title、chars、lines、updated。number 在 schema 的 required 之外,仅在解析出章号时出现。
| 字段 | 类型 | 内容 |
|---|---|---|
book | string | 清洗后的书名(与传入值可能不同) |
file | string | 章节文件名,含扩展名 |
number | integer | 解析出的章号;可选,解析失败时该键不存在 |
title | string | 文件名去掉扩展名、再去掉章号段与分隔符后的标题;文件名没有标题时回退读正文一级标题(v5.0.0 修正:如工具建的 第07章.md 内有 # 靠岸之后 → title 为「靠岸之后」);两者都取不到时为空串 |
chars | integer | 该章全文的 text.length(UTF-16 码元) |
lines | integer | 该章行数,空文件为 0 |
updated | string | 文件 mtime 的 ISO 8601 字符串(UTC) |
渲染层(formatChapters)每行格式为 - 第NN章 标题 — M 字 / L 行 (文件名, 更新于 YYYY-MM-DD);number 缺失时章号位显示 ?,日期只取 updated 的前 10 个字符。列表之后,若存在同章号多文件,追加 ⚠️ 同章号多文件 N 组 与逐组的 第NN章: 文件A / 文件B(v4.3.1 起)。
行为与实现
1 · 文件枚举与过滤
对 bookDir(root, book)(即 <root>/novels/<清洗后书名>)执行 readdir({ withFileTypes: true }):非文件条目跳过,扩展名(小写后)不在 {.md, .markdown, .txt} 中的跳过。目录不存在或不可读时 scanChapters 抛出带完整路径的友好错误(提示可用 novel_books 核对书名),而不是返回空列表——这一点与 novel_books 的静默空数组不同。
2 · 章号解析 parseChapterNumber
入口先做字符宽度归一:normalizeChapterWidth 把全角数字 0-9 与全角 -./ 按 −0xFEE0 逐字符映射为半角,再剥掉扩展名得到 stem。归一化不使用整串 NFKC,因为 NFKC 会把标题里的中文句号「。」也归一成「.」,随后被标题清洗的 [_\-.]+ 换成空格(「初遇。重逢」被破坏),且长度变化会破坏依赖索引切片的文件名解析。逐字符映射保持 1 字符换 1 字符、索引一一对应。
五条规则按序短路,先命中先返回;因此同一文件名可能被前一条规则解释成不同数值。中文章号由 cjkToNumber 累加:
3 · 标题清洗 cleanChapterTitle
同样先做宽度归一,再依次剥离 第?(\d{1,4})[章回话]、第[零一二三四五六七八九十百千万两]+[章回话]、行首的 LEADING_NUMBER(/^第?(\d+)[章回话]?[\s._\-—]*/),把 [_\-.]+ 换成空格后 trim。无标题章节返回空串,不回退成文件名(v3.6.0 行为)。v5.0.0 修正:cleanChapterTitle 本身不变,但工具层在「文件名没有标题」时改为回退读正文一级标题(firstHeadingTitle,同样剥掉章号段),因此不再出现「novel_new_chapter 明明写了 title,novel_chapters 却显示空标题」;一级标题只是章号本身(# 第07章)时仍返回空串,不把章号当标题重复显示。
4 · 排序
章号升序,解析不出章号的文件排到最后;同章号(或同为无章号)时按文件名 localeCompare。不做去重:两个文件解析出相同章号时会同时列出。
5 · 统计量 chapterStats
行数走 countTextLines 这一唯一出口,保证与 novel_read 的 totalLines 口径一致(空文件 0 行)。文本读取先经 decodeTextBuffer 做编码探测(BOM → NUL 占比 → 严格 UTF-8 → UTF-16 双阈值 → GBK),探测失败抛错,抛错时本工具不吞异常——novel_chapters 未对该调用加 try/catch,单章编码异常会让整次调用失败(MCP 形态下呈现为 isError: true 的文本帧)。
6 · 归一化键的归属
normalizeChapterKey(把 2 / 第2章 / 第02章.md / 第01章 归一到 第02章 字符串)不由本工具调用,它服务于 novel_summary 的摘要槽位对齐。本工具只回传整数 number,不返回归一化键。两者现已对齐字符类(均含「千万」),但形态不同(整数与补零字符串),调用方自行对齐时需注意。
约束与边界
book必填且被清洗:sanitizeSegment剥离\ / : * ? " < > |、首尾的点与空白;结果为空时报「参数 book 不能为空(清洗后无有效字符)」;与 Windows 保留名(CON/NUL/COM1等)同名时加前置下划线。因此书名必须与目录名精确匹配,清洗后的值还会回填到返回体的book字段。- 目录缺失是硬错误:抛出
书库中未找到作品目录:<路径>…,而非空数组。调用方应把该错误当作「书名或根目录错误」处理。 number可能不存在:文件名不含可识别章号时该键被整体省略(实现上用条件展开写入)。调用方必须按可选字段处理,不能假设chapters[i].number总有值;排序也会把这类章推到末尾。- 含「万」的中文章号解析失败:正则字符类收录了「万」,但
cjkToNumber未定义「万」的单位累加,逐字扫描遇「万」直接返回undefined。故第两万章.md得不到number。第零章.md同理(既无单位、section又为 0)。 - 同号重复文件不合并,但会在渲染文本里被点名(v4.3.1 起):两个文件解析出同一章号时返回体仍出现两条相同
number的记录(不去重、不改文件),但渲染层会追加同章号提示分组。这类书用章号定位的工具(novel_read等)会直接报错,必须改用文件名指定,或先把多余文件改名到空闲章号。 - 列表无截断,成本为全文读取:章节数、字段长度都没有上限;每章都要
stat+ 读全文,全部章节一次返回。删除或新增章节文件会立即反映,因为结果不做缓存。 chars不等于「汉字数」:它是text.length,Markdown 标题行、空行、标点、ASCII 一律计入;lines按\r?\n计行,空文件为 0 行。updated是 UTC 时间串:形如2024-05-01T09:30:00.000Z,渲染层只截前 10 字符。跨时区展示需自行换算。- MCP 形态的
root越界静默回退:越界值被替换为启动时确定的书库根并只记 stderr 日志,返回体不含任何越界提示。 - 可被总开关关闭:执行前经
assertToolEnabled校验状态文件(~/.dsh/dsh-novel-writer/state.json),关闭后直接抛错。
相关工具
- novel_books —— 先列出全部作品名,避免因书名不精确匹配而触发本工具的目录缺失错误。
- novel_read —— 用本工具给出的章号 / 文件名 / 标题读取正文,二者共用同一套章号解析与行数口径。
- novel_new_chapter —— 新建章节文件,章号由
nextFreeChapterFile复用同一解析逻辑判定占用。 - novel_summary —— 章节摘要按
normalizeChapterKey归一化键对齐,与本工具的整数章号是两种形态。
源码位置
- lib/index.js —— 工具注册与
execute实现(registerNovelChapters,参数校验、逐章统计、结果组装) - lib/core.js ——
scanChapters(枚举、扩展名过滤、章号解析、标题清洗、排序)、parseChapterNumber、cjkToNumber、normalizeChapterWidth、cleanChapterTitle - lib/core.js ——
chapterStats(chars/lines/size/updated)、countTextLines、decodeTextBuffer、formatChapters(渲染) - lib/core.js ——
normalizeChapterKey(归一化键,供novel_summary使用)、sanitizeSegment、bookDir - mcp/server.mjs ——
buildArgs(root注入与越界回退)