上游:prd.md(同目录)。行为依据 multi-agent spec v3.5 §5.5/§5.8/§5.9/§8/§9、story-repo spec 0.9 §2.0/§2.1、实施计划 0.3 M5。
新增/改动集中在五处,互不越界:
| 模块 | 职责 | 不做 |
|---|---|---|
bin/webnovel-writer.js |
命令分级(工作目录级 / 书级)+ 工作目录定位 | 业务逻辑 |
src/installer/ |
init/update 编排:环境检测、vendor 复制、模板哈希清单、壳落位、AGENTS.md 标记块 | 不联网、不改全局配置、不建 git 仓库 |
src/session/(扩展) |
books.jsonl 写侧:登记 / 换书 / 最后打开;与 M4 读侧同模块,格式单源 | 不碰书仓库内容 |
src/commands/(新增 12 个) |
F1 缝 + 多本书 + 安装器的 CLI 薄壳,全部委托用例层 | 不自带业务判断 |
skills/ + src/host-shells/ + adapters/ |
命令引用语法模板变量、SKILL.md 写章流程接 F1 通道、registry 扩字段、hook 接线 | 生成器仍不联网、确定性不变 |
命令模块新增可选导出 export const scope = 'workdir'(缺省 'book')。bin 启动流程:
--version / --help / 无命令:现状不变。scope === 'workdir' 的命令(init / update / list-books / switch-book / session-context):ctx 给 { workdir: process.cwd(), packageRoot },不建 CacheManager(init 时书都不存在)。scope === 'book' 的命令,按序判定 repoPath:
book.yaml → 书仓库直启,repoPath = cwd(兼容现有全部测试与开发场景);.webnovel/ → 读 books.jsonl(复用 M4 readBooksRegistry + 自愈),取「当前」书 → repoPath = cwd/<目录>,并更新该书「最后打开」;无当前书 → 人话指引(「说“建书”开始第一本 / 用 switch-book 选一本」)退出码 1;persist-book 声明 scope = 'workdir-or-book'——建书时书目录还不存在(见 §3.4)。.webnovel/ 内容(vendoring,spec §2.0 既定).webnovel/
├── bin/webnovel-writer.js # 与包同源复制
├── src/** # 运行时全量
├── roles/*.md # 角色任务书单源(三级宿主兜底可读,spec §2.0「角色定义」)
├── node_modules/js-yaml/** # 唯一运行时依赖,从安装器自身的解析位置复制
├── package.json # 供 --version
├── manifest.json # 模板哈希清单(§3.2),installer 写
└── books.jsonl # 用户数据,installer 只在缺失时建空文件,永不覆盖
import.meta.url 上溯),js-yaml 用 createRequire(import.meta.url).resolve('js-yaml/package.json') 定位后整目录复制——npx / npm pack / repo 内直跑三种形态同一逻辑。skills/ adapters/ templates/ 不进 .webnovel/:它们只在 init/update 时由当前运行的包消费。vendored 副本里跑 update 时检测到自己位于 .webnovel/ 下,提示改用 npx webnovel-writer update(自己更新自己是空转)。detect_bin(PATH 探测的可执行名:claude / codex / gemini / cursor-agent)、install_dir(.claude / .codex / .gemini / .cursor)。validator 同步校验一二级宿主必有这两字段。install_dir:skills/webnovel-writer/SKILL.md、agents/<角色>.md|.toml。hasHooks: true)额外接线 SessionStart:.claude/settings.json 的 hooks.SessionStart 加一条 node .webnovel/bin/webnovel-writer.js session-context。settings.json 已存在时做保留式合并(只增本项,按 command 字符串幂等判重);不存在时新建最小结构。settings.json 不进哈希清单(用户主权文件,update 只做同样的幂等合并)。PATH 各目录找 detect_bin(win32 追加 PATHEXT 扩展名);纯函数 + 注入 env,可测。--hosts=a,b 显式覆盖探测;一个都没检测到 → 只装公约数层(AGENTS.md + .webnovel/),报告里指引用 --hosts 补装。cmd: 'node .webnovel/bin/webnovel-writer.js';SKILL.md 里所有 webnovel-writer <子命令> 改写 {{cmd}} <子命令>。各宿主当前同值,仍走变量(spec §5.9)。review-input → 两审(subagent 或顺序自审)→ save-review,第 4 步接 finalize;「继续」接 next --json(AI 吃 DTO)。drift check 逻辑不变(仍是确定性对比)。通则:JSON 输入一律 --file=<路径>(UTF-8 文件,杜绝 stdin 编码雷区);stdout 输出人话或 JSON(--json 时);失败人话报错 + 退出码 1,永不带栈。
| 命令 | 入 | 出 | 委托 |
|---|---|---|---|
next [--json] |
— | --json:完整 {ok, gitHealth, 序, state, needsAI, message, dto};缺省人读不变 |
determineNextState |
review-input <章号> [--draft=路径] |
draft 缺省 工作区/草稿-A.md(与 mechanical-check 一致) |
写 工作区/审稿输入.json,stdout 报路径(大 JSON 走文件,宿主用读文件工具吃) |
assembleReviewInput |
save-review <章号> --file=<两审json> |
{factCheck, editorial, mode?, 待确认新专名?, 章摘要?} |
schema 校验→合并→落 工作区/审稿.md + 评审报告/,stdout 报阻断数与路径 |
从 runReviews 抽出 saveReviews(校验+合并+落盘),两处共用不双写 |
persist-outline --file= |
{细纲} |
落 工作区/细纲.md |
persistDraftOutline |
persist-book --file= [--dir=目录名] |
{book, 总纲, 卷纲} |
建书目录(workdir 模式下 --dir 或书名)+ 落盘 + git init + 指路 AGENTS.md + books.jsonl 登记并置当前 |
persistCreateBook(扩展)+ registerBook |
persist-volume-review --file= |
{卷号, 卷摘要, 下卷卷纲?, 伏笔条目?} |
落卷摘要/下卷卷纲/伏笔条目 | persistVolumeReview |
persist-repair --file= |
{repairs:[{file,content}]} |
安全网校验后写回(allowedFiles = 当前检测失败清单,命令内现算) | persistRepair + detectParseFailures |
finalize <章号> --payload=<json路径> |
定稿包(finalizeChapter payload 全字段) |
原子 commit + 缓存刷新,stdout 报 commit 与下一步 | finalizeChapter |
多本书命令:list-books(书单+当前标记)、switch-book <书名>(改「当前」+ 最后打开;模糊匹配失败列候选)、session-context(输出 assembleSessionContext().text,hook 与无 hook 宿主同源,注入逐字一致)。
{"书名": "...", "目录": "...", "当前": true|false, "最后打开": "2026-07-03"}——M4 读侧已容忍未知字段;写侧保证单一「当前」。扫描重建行缺「最后打开」可(重建即丢,属可再生信息)。
{ "version": "7.0.0-alpha", "files": { "<工作目录相对路径>": "sha256-hex" } }
books.jsonl(用户数据)、.claude/settings.json(合并式,见 §1.3)。AGENTS.md 记标记块内容的哈希(update 只动块内,块外用户区不参与判改)。--force 覆盖);文件缺失 → 重建。init 检测到 manifest.json 存在 → 直接走 update 语义并在报告言明。装到哪、Node 版本判定、检测到/未检测到的宿主与支持等级(tier + support.md 口径)、降级说明(无 subagent → 兼容模式声明)、下一步(「打开 <宿主> 对它说:开始写书」)。
--dir 或 payload 书名 → workdir/<目录>/;已存在同名目录且含 book.yaml → 人话报错防覆盖。落盘成功后 registerBook 置当前。repoPath = cwd 行为不变;next 缺省输出不变;runReviews 对外签名不变(内部抽 saveReviews)。package.json 加 files 白名单(bin/src/roles/skills/adapters/templates),npm pack 产物即安装源——CI 用 pack 产物验收(PRD Q1 决策:真发 npm 推迟 beta)。persistCreateBook 新增指路 AGENTS.md 落盘:main-loop 测试建书断言不受影响(新增文件,不改旧文件)。| 决策 | 取 | 舍与理由 |
|---|---|---|
| 运行形态 | vendoring 进 .webnovel/(spec 既定) |
npx 每次解析:离线不可用、版本漂移、可诊断性差(v6 「装哪了/读的哪份」教训) |
| JSON 输出 | next --json 走 stdout;review-input 落文件 |
ReviewInput 含草稿全文(大),落文件让宿主用读文件工具分段吃;DTO 小,stdout 管道友好 |
| CLI 探测 | PATH 纯函数探测 + --hosts 覆盖,非交互 |
交互问答会卡死「AI 替作者跑 init」的主场景 |
| 未检测到宿主 | 只装公约数层 + 指引 | 无条件全装偏离 spec「按检测生成」,且掩盖探测失败 |
| settings.json | 幂等合并,不进哈希清单 | 全量覆盖会毁用户自己的 hooks/权限配置 |
| update 冲突 | 默认跳过列清单 + --force |
交互确认在非交互环境挂死;静默覆盖违反 spec §8.2 |