NovelNovel
DeepSeek Harness 插件:让 agent 用 novel_* 工具写小说(章节 / SillyTavern 角色卡 / 世界观词条 / 写作预设 / 上下文组装 / 导出),数据落工作区文件
- Stars
- 0
- Language
- TypeScript
- Created
- Aug 20, 2026
- Updated
- Sep 18, 2026
Introduction
dsh-novelnovel · DeepSeek Harness 插件
用工作区里的普通文件写小说:agent 直接用 novel_* 工具建作品、写正文、导角色卡、维护世界观词条、
检索与导出,续写由 harness 的模型承担,插件负责把作者的设定(世界观 / 命中词条 / 参与角色 /
写作预设 / 前文摘录)组装成写作简报交给它;方法层面的东西(文风、对话、节奏、结构)由自带的技能承载,
也可以导入你自己的。
本仓库就是插件包(dsh.bundle.patch → cordis.patch.yml,向 profile 插入一行 id: novelnovel)。
领域逻辑(角色卡解析、预设解析、技能包解析、提示词组装、词条关键词匹配、章节排序、全文搜索、
PNG 卡读写、字数统计)集中在 src/domain/,与 harness 侧的存储/注册代码分离。
安装
npm install
npm run build # 构建插件产物 lib/(不入库,改了 src/ 必须重建)
# 装进一个 dsh profile(不存在会自动初始化)
dsh plugin --profile novelnovel add .
# 验证:在真实 harness 服务上跑通全部工具(不调用模型)
npm run test:dsh # 默认 profile: novelnovel
DSH_PROFILE=web npm run test:dsh # 指定其它 profile
想装到已有的 web profile,把 profile 名换成 web 即可。
分发时用 npm pack 产出的 tarball 安装:插件会被复制进 profile 的 node_modules,不依赖本仓库工作树。
兼容性
已在两套环境验证(npm run test:dsh 40 项全过 + 真实 headless 会话):
@deepseek-ai/dsh@0.1.5-rc.1(npm 上的latest,其子包解析为0.1.5-rc.2)@deepseek-ai/dsh@0.1.2-rc.1CLI +0.1.3-alpha.2子包(本机 source checkout 环境)
package.json 的 peerDependencies 用通配 *:插件运行时由 src/harness.ts 解析 harness 自己的包实例
(保证与运行中的 harness 同模块实例),不在依赖层面锁版本。src/contract.ts 镜像的 API 面已在
0.1.3-alpha.2 与 0.1.5-rc.2 之间逐项比对,无签名差异——升级 harness 后重跑一次
npm run test:dsh 即可确认。
用法
在装了插件的 profile 里开一个会话,直接用自然语言提写作需求即可;系统提示词会引导 agent 使用这些工具:
| 工具 | 作用 |
|---|---|
novel_project | 作品的新建 / 列表 / 详情 / 改设定(简介、世界观、写作要求)/ 切换当前作品 / 删除 |
novel_chapter | 章节列表、读取、新建、追加(写正文的入口)、覆写、改名、打标签、排序、检索、删除 |
novel_character | SillyTavern 角色卡(PNG/JSON,V1/V2/V3)导入、查看、参与开关、再导出 PNG、移除 |
novel_lorebook | 世界观词条维护(带关键词=命中才注入,无关键词=常驻注入) |
novel_preset | 写作预设导入(SillyTavern JSON)/ 手写 / 编辑 / 激活 / 停用 |
novel_skill | 项目级写作方法技能:列出 / 导入技能包(JSON)/ 导出分享 / 删除 |
novel_context | 写作前必调:组装本次的写作简报(系统提示词 + 指令块 + 注入清单 + 参与角色) |
novel_export | 整书导出 Markdown / TXT,或全量备份 JSON |
另外自带 6 个技能,agent 按需加载(/novel-prose 之类的手势也能直接触发):
| 技能 | 管什么 |
|---|---|
novel-writing | 工具流程与连续性检查(先读简报、写正文、落盘、记设定) |
novel-cards | 角色卡 / 预设的导入语义与宏规则 |
novel-prose | 文风与叙述:视角一致性、叙事距离、句式节奏、去 AI 味 |
novel-dialogue | 对话:潜台词、说话人辨识度、标签与动作拍、避免翻译腔 |
novel-scene | 场景与节奏:场景 vs 概述、目标/阻碍/转折、章末钩子 |
novel-outline | 结构与大纲、伏笔回收、通读一致性审计、中文网文套路 |
技能是按需加载的:模型平时只看得到每个技能的名字和一句描述,判断相关了才把正文拉进上下文。 所以这些方法平时不占上下文,也不会和你的写作预设打架——需要的时候它们才出现。
装你自己的写作方法
项目根的 .dsh/skills/ 是 harness 自己的技能目录:它会被自动扫描、改动实时生效,而且优先级高于
插件内置技能(项目 rank 100/200,插件 rank 250)——所以同名时生效的是你那一份。
你可以直接往里放 SKILL.md,也可以让 agent 用 novel_skill 导入一个技能包:
// 技能包:可以含多个技能;也可以去掉 skills,把单个技能写在顶层
{
"name": "我的爽文写法", // 包名,给人看的,可含中文
"author": "you",
"version": "1.0.0",
"skills": [
{
"name": "fast-payoff", // 必须是 kebab-case,其它形式 harness 会拒绝
"description": "Move the payoff closer to the setup.", // 模型唯一能看到的字段,写成路由判据
"whenToUse": "爽点 / 铺垫太长 / 节奏太慢", // 可选,补充触发场景(中文触发词要留在这里)
"content": "# 爽点前移\n\n正文,Markdown。" // 技能正文
}
]
}
novel_skill action=import path=my-pack.json 落到 .dsh/skills/,harness 实时收录
novel_skill action=list 现在装了哪些、哪些覆盖了内置技能
novel_skill action=export name=fast-payoff 导出成包,可以分享给别人
novel_skill action=remove name=fast-payoff confirm=true
导入的技能和你手写的技能走完全相同的加载路径——插件只负责摆文件,加载是 harness 自己的事,
所以插件里没有第二套技能机制。novel_skill 也只删自己导入的(按 .novelnovel-skill.json 标记识别),
手写的技能它一律不覆盖、不删除。
需要知道的一件事:harness 取的项目根是「最近的含 .git 的祖先目录,没有就用当前工作目录」。如果会话
开在仓库的子目录里,技能该放的地方就在工作区之外了——这时 novel_skill 会拒绝并说明原因,
而不是写到一个不会被扫描的位置。
斜杠命令 /novel:直接打印当前作品状态(/novel list 列出全部,/novel <作品> 切换当前作品),
不经过模型。
一小段系统提示词(order 4500),说明这个工作区里的小说该怎么写。
典型流程(agent 视角):
novel_context chapter=3 instruction="写林晚在城墙下遇到祭司" → 拿到简报
(按简报写正文)
novel_chapter action=append chapter=3 text="…" → 落到章节
novel_lorebook action=add name=祭司 keys=夜祷 content=… → 记录新设定
novel_context 返回的系统提示词区块是设定与文风的权威来源:它就是给模型的 system prompt,
由 harness 自己的模型来遵守。
配置
在 profile 的 cordis.patch.yml 里按行 id 覆盖,无需改插件包:
- id: novelnovel
name: dsh-novelnovel
config:
dataDir: .novelnovel # 数据根目录(相对工作目录,默认 .novelnovel)
defaultPrevChapterCount: 2 # 简报默认携带的前文章节数(默认 1)
defaultPrevChapterChars: 2000 # 每个前文章节摘取的尾部字数(默认 1500)
defaultRecentChars: 4000 # 当前章摘取的尾部字数(默认 3000)
非法配置(绝对路径、..、负数)在加载期直接抛错,不静默取默认值。
数据布局
<工作目录>/.novelnovel/
workspace.json 当前作品
projects/<projectId>/
project.json 标题 / 简介 / 世界观 / 写作要求
lorebook.json 世界观词条
presets.json 写作预设与激活项
chapters/index.json 章节元数据(标题、标签、顺序)
chapters/<chapterId>.md 章节正文(Markdown,可直接用 read/write 工具编辑)
characters/<id>.json 角色卡(rawData 无损保留)
characters/<id>.<ext> 原始头像(扩展名与真实媒体类型一致)
exports/ 导出结果
skillpacks/ novel_skill 导出的技能包
<项目根>/.dsh/skills/ 项目级技能(harness 自己扫描,含 novel_skill 导入的)
<技能名>/SKILL.md
<技能名>/.novelnovel-skill.json 导入标记(手写的技能没有它)
正文独立成文件是有意的:长篇小说用 read/write 工具直接改正文比走工具更顺手,
而 novel_chapter action=list 会重新读文件统计字数,所以绕过插件直接改文件也不会失同步。
技能不放在数据目录里,因为它是整个工作区的写作方法,不属于某部作品——见「装你自己的写作方法」。
设计说明
- 写入路径:文本读写一律走
ctx.fs(受 harness 沙箱与权限策略约束、与write/edit共用版本语义,并emit('fs/observed')让变更流与「先读后写」策略保持一致)。ctx.fs没有删除与二进制写入能力,删文件与写 PNG 头像用node:fs—— 这条路径不受沙箱约束, 所以插件自己兜住:目标必须落在会话工作目录内,否则直接拒绝(novel_character action=export out_path=<工作区外>会报错而不是写出去)。 - 损坏的数据文件:
project.json读不出来的作品目录会被跳过并在novel_project action=list里点名(不让一个坏目录把整个插件堵死);workspace.json损坏时同样不致命,但此时若有多部作品, 解析「当前作品」会要求显式传project=——宁可报错也不猜,避免把正文写进错误的作品。 例外是chapters/index.json:它损坏时直接报错,因为静默当成空索引会让下一次建章覆盖掉整份目录。 数据文件都是给人手改的,读 JSON 时容忍 BOM 与 CRLF。 - harness 依赖不内联:
@deepseek-ai/*是 peer,运行时由 harness 自己提供——服务按模块实例注册, 内联第二份会重复注册。以link:方式安装时包位于工作区之外,Node 从包 realpath 找不到 harness 的依赖闭包,所以src/harness.ts会依次尝试:普通 import → 从 harness 进程入口解析 → 从$DSH_HOME/profiles与工作目录解析,并保证与运行中的 harness 是同一个模块实例。 - API 契约副本:
src/contract.ts是手写的最小类型契约(只含本插件用到的成员), 依据@deepseek-ai/dsh@0.1.2-rc.1 / 0.1.3-alpha.2的真实签名整理。这样插件源码不依赖 harness 的包解析路径就能通过tsc;运行时行为由真实 harness 与端到端验证保证。 - 技能注册:用
ctx.skills.register运行时注册(rank 250),而不是去改skill-filesystem的customSkillDirs——后者是别的插件行的 config,覆盖它要重述 dsh-base 的整份配置(patch 是整行替换), 极易随上游变化失效。副作用是项目级技能(rank 100/200)天然覆盖同名插件技能,正好成了 「装你自己的写作方法」那条路径。 - 技能写在
<dataDir>之外:技能属于整个工作区,不属于某部作品,所以落在项目根的.dsh/skills/(harness 自己的目录)。项目根按 harness 的口径取「最近的含.git的祖先目录,没有则用工作目录」; 会话开在仓库子目录里时项目根在工作区之外,此时novel_skill拒绝并说明原因,而不是写到一个 不会被扫描的地方——「工具报成功、技能却不生效」是最难查的一类问题。这是插件唯一往数据目录之外 写文件的地方,范围由ctx.fs.contains收紧;导入的技能文件与手写技能走同一条 harness 加载路径, 插件只摆文件,不维护第二套机制。 - 破坏性操作:删除作品 / 章节 / 角色卡都需要
confirm=true,插件会拒绝未确认的调用, 提示先与用户确认。
开发
npm run build # esbuild 打包到 lib/
npm run typecheck # tsc -p .(严格模式,零错误)
npm test # 5 个纯逻辑单测(卡解析 / 预设+提示词 / 检索 / 排序 / 技能包)
npm run test:dsh # 端到端验证(需要已安装的 profile)
npm run test:dsh 会拉起真实 harness 服务(SystemPrompt + ToolRuntime + LocalFileSystem +
SkillRegistry + observation policy)并驱动全部工具:作品/章节/词条/角色卡(含 PNG 双写回读)/预设/
简报组装/关键词注入命中与未命中/检索/导出/技能注册(按 skills/ 里的 SKILL.md 逐个核对)/项目级技能
(技能包导入→列出→导出→再导入→删除,含手写技能不被覆盖、校验失败不写一半)/命令处理器/配置校验/
工作区边界与损坏文件容错/观察记录归属/卸载清理/系统提示词段,共 40 项检查,不调用模型。
npm test 是它的快速补充:5 个纯逻辑单测直接测 src/domain/ 里的解析与组装函数。
tests/perf-probe.mjs 是性能探针(在 profile 目录里跑):铺 3 部 × 200 章,
打印每个工具调用的耗时与 ctx.fs 调用次数。解析作品引用只读元数据、统计字数才读正文,
这条边界靠它守住——把正文读回解析路径会让一次 action=append 的调用数从 39 涨到 1839。