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

novel_books

列出章节库中的全部作品:枚举 novels/ 下的子目录,逐本调用章节扫描与逐章统计,返回书名、章节数与总字数。无必填参数(仅可选 root),是章节库的入口查询。只读、不落盘任何文件,但每本书都要逐章读取全文才能给出字数,IO 量随全库章节总数线性增长。

参数

参数类型必填说明
rootstring章节库根目录(含 novels 子目录)。省略时按 config.root → 会话工作目录的顺序回退。

参数 schema 声明 additionalProperties: false,即只接受 root 一项;不存在按书名过滤的参数,单本查询应改用 novel_chapters

输出结构

契约字段(output.schema):rootbooks 为必返;books 的每个元素必须同时含 namechapterschars,三者均为必返字段。

字段类型内容
rootstring本次实际生效的章节库根目录
booksarray作品列表,元素见下三行
books[].namestring书名,即 novels/ 下的目录名原文(未做清洗)
books[].chaptersinteger该目录下通过扩展名过滤的章节文件数量
books[].charsinteger逐章字数累加值,见下文口径说明

渲染层(output.renderformatBooks)输出为 <path> / <type> / <content> 包裹的纯文本,每行格式为 - 书名: N 章, M 字;列表为空时写入「(暂无作品。请在 novels/<书名>/ 下存放章节文件,如 第01章.md)」。

行为与实现

1 · 根目录解析

resolveRoot 按三级回退确定根目录:参数 root(非空字符串)→ config.root → 会话工作目录 exec.agent.session.header.cwd,最后兜底 process.cwd()。工具自身不校验根目录范围;越界限制由 MCP 层承担(见「约束与边界」)。

2 · 一级目录枚举与过滤

novelsDir(root)(即 <root>/novels)执行 readdir({ withFileTypes: true }),随后按序过滤:不是目录的条目跳过(普通文件直接忽略);目录名以 . 开头的跳过(v3.5.0 #27,排除 .git 与备份目录);目录名恰为 创作资料 的跳过(v3.1.0,该目录归 novel_outline 使用)。此目录枚举失败(目录缺失、无读权限)时不抛错,直接返回 { root, books: [] }

3 · 章节扫描与字数累加

每本候选作品调用 scanChapters:仅收录扩展名属于 .md / .markdown / .txt 的文件,章节数即为该列表长度。字数按章累加:

chapters(book) = |scanChapters(<root>/novels/<book>)| chars(book) = Σ chapterStats(book, chᵢ).chars ← chars = 该章全文的 text.length 逐章 try/catch:统计失败的章节静默跳过(不中止整本)

两处失败路径的粒度不同,必须区分:scanChapters 抛错(目录无法读取)时整本跳过,该书不出现在结果中;而单章 chapterStats 抛错时只跳过该章的字数chapters 计数不变。因此 chapters(列表长度)与 chars(逐章读取成功之和)不同源,个别章读取失败会让两者口径不一致。

4 · 排序

books.sort((a, b) => b.chars - a.chars || a.name.localeCompare(b.name))

总字数降序为主键,同字数时用 localeCompare 比较书名。后者依赖宿主 locale,排序结果不保证跨环境一致。

约束与边界

  • 无必填参数,但 schema 封闭:只接受 root,多余参数违反 additionalProperties: false 的声明。
  • 空结果不可区分原因novels/ 不存在、无权限、或确实没有作品目录,三种情形都返回 books: [],没有错误码或提示字段。调用方不得据「空列表」判定「库是空的」,更不能判定路径正确。
  • 两处静默排除:以 . 开头的目录与名为 创作资料 的目录不会出现在 books 中;在这两个位置存放的章节文本无法经本工具发现。
  • 无分页、无数量上限books 数组与所有整型字段都没有上限或截断规则,返回体大小随作品数线性增长。
  • 成本等价于全文读取:每本书都要逐章 stat + 读全文以计算 chars,IO 量为全库章节文件总数;库越大耗时越长,且除了宿主取消信号外没有增量或缓存机制。宿主取消信号会经 exec.signal 传入 readFile
  • chars 是 UTF-16 码元长度,等于 String.prototype.length,不是汉字个数:Markdown 标题行、空行、标点、ASCII 字符都计入,且以换行符分隔。它只是字数近似值,不可当作出版字数口径。
  • chapters 含 0 也可能出现:目录存在但无合法扩展名文件时,该书仍被列出且 chapters 为 0——「作品存在」与「有章节」是两件事。
  • MCP 形态的 root 越界是静默回退:调用方给的 root 必须落在启动参数 --root(缺省读 DSH_NOVEL_WRITER_ROOT,再缺省 process.cwd())之内;越界时回退到书库根并只记 stderr 日志,返回体里的 root 才是真实生效值。应以后者为准,不要用 root 参数探测文件系统。
  • 可被总开关关闭:执行前先经 assertToolEnabled 读取状态文件(~/.dsh/dsh-novel-writer/state.json,或环境变量 DSH_NOVEL_WRITER_STATE 指定的路径);在写作助手 UI 中关闭后本工具直接抛错,不返回结果。

相关工具

  • novel_chapters —— 取单本作品的章节清单(章号、标题、字数、行数、更新时间)。
  • novel_read —— 按章号 / 文件名 / 标题定位并阅读正文。
  • novel_import —— 批量导入外部稿件并按书名分组落到 novels/<书名>/
  • novel_keywords —— 按全书或单章统计高频关键词。

源码位置

  • lib/index.js —— 工具注册与 execute 实现(registerNovelBooks,参数 schema、输出契约、枚举与排序)
  • lib/core.js —— novelsDir(根下 novels 拼接)、scanChapters(扩展名过滤与章号解析)、chapterStatschars / lines / size / updated)、resolveRootformatBooks(渲染)
  • lib/core.js —— CHAPTER_EXTENSIONS.md / .markdown / .txt 常量)
  • mcp/server.mjs —— resolveLibraryRoot / isInsideRoot / buildArgs(书库根三级来源与 root 越界回退)