novel_books
列出章节库中的全部作品:枚举 novels/ 下的子目录,逐本调用章节扫描与逐章统计,返回书名、章节数与总字数。无必填参数(仅可选 root),是章节库的入口查询。只读、不落盘任何文件,但每本书都要逐章读取全文才能给出字数,IO 量随全库章节总数线性增长。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
root | string | 否 | 章节库根目录(含 novels 子目录)。省略时按 config.root → 会话工作目录的顺序回退。 |
参数 schema 声明 additionalProperties: false,即只接受 root 一项;不存在按书名过滤的参数,单本查询应改用 novel_chapters。
输出结构
契约字段(output.schema):root 与 books 为必返;books 的每个元素必须同时含 name、chapters、chars,三者均为必返字段。
| 字段 | 类型 | 内容 |
|---|---|---|
root | string | 本次实际生效的章节库根目录 |
books | array | 作品列表,元素见下三行 |
books[].name | string | 书名,即 novels/ 下的目录名原文(未做清洗) |
books[].chapters | integer | 该目录下通过扩展名过滤的章节文件数量 |
books[].chars | integer | 逐章字数累加值,见下文口径说明 |
渲染层(output.render → formatBooks)输出为 <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 的文件,章节数即为该列表长度。字数按章累加:
两处失败路径的粒度不同,必须区分:scanChapters 抛错(目录无法读取)时整本跳过,该书不出现在结果中;而单章 chapterStats 抛错时只跳过该章的字数,chapters 计数不变。因此 chapters(列表长度)与 chars(逐章读取成功之和)不同源,个别章读取失败会让两者口径不一致。
4 · 排序
总字数降序为主键,同字数时用 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(扩展名过滤与章号解析)、chapterStats(chars/lines/size/updated)、resolveRoot、formatBooks(渲染) - lib/core.js ——
CHAPTER_EXTENSIONS(.md/.markdown/.txt常量) - mcp/server.mjs ——
resolveLibraryRoot/isInsideRoot/buildArgs(书库根三级来源与 root 越界回退)