erlingmijiang/dsh-lorebooklike-prompt-tool ↗★ 0
dsh-lorebooklike-prompt-tool
声明式提示词注入与改写工具,支持类世界书规则编辑。 适合进行复杂提示词工程调优的用户,不适用于角色扮演或小说创作场景。
安裝
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:erlingmijiang/dsh-lorebooklike-prompt-tool說明文件
閱讀完整 README ↗dsh-lorebooklike-prompt-tool
DSH的声明式提示词注入层。 把什么时候、往提示词的哪个位置、以多大把握、注入或改写哪段文本变成一份可编辑、可热重载、可复现的 JSON 规则文件,搭配一个类酒馆世界书的图形编辑器。
注:这不是酒馆的dsh插件或替代前端、不是酒馆模拟器、不和角色卡交互、不适用于角色扮演和小说创作场景——它是提示词工程工具。
灵感来源与致谢
这个插件的四项核心机制 全部源自 SillyTavern 的世界书机制。
| 从酒馆借鉴的机制 | 本插件的对应物 |
|---|---|
| 🔵 Constant(蓝灯)/🟢 Selective(绿灯) | mode: "constant" / mode: "selective" |
| Primary Keys + Secondary Keys | keys + keySecondary |
| Secondary 逻辑:AND Any / AND All / NOT Any / NOT All | keyLogic: "any" / "all" / "notAny" / "notAll" |
| Scan Depth(回看最近 N 条消息) | scanDepth |
| Probability(概率触发) | probability |
| Content 上的 Find/Replace 正则(含捕获组) | replaces |
/pattern/flags 字面量写法,且正则优先于大小写与全词设置 | 同样的优先级规则 |
| 「绿灯没给主键就永不触发」这种安全默认 | 同样是默认 |
版权声明:本仓库不含任何一行酒馆代码,只参考其被开源社区验证过的行为逻辑和设计哲学。
(以下全部内容均由蓝色大肥鱼书写)
设计边界
- 不新增会话事件类型——所以旧版本 harness 仍能加载本插件产生的日志。
- 不改
agent-loop,不动任何框架源码。 - 所有注册走
ctx.effect()/ 注册表 API,卸载时 section 会整批注销(冒烟测试里有专门一项断言)。 - 不注册任何工具。规则完全由人工编排——一个能自助改写注入规则的 Agent 不再可审计。
- 零
@deepseek-ai/*运行时依赖,只需要node:*。 - 不做角色卡、不做 RP 前端、不做记忆——那些是别的插件的事。
和酒馆的区别
拿掉角色扮演这个使用背景之后,酒馆的很多设计在 DSH 里直接照搬是行不通的,这是宿主的注入面不同导致的必然结果。
| 能力 | 含义 |
|---|---|
| 分层提示词 | 每个条目声明一个语义化层锚点,精确插进系统提示的指定位置,与 DSH 自己的段落交错,而不是一律堆在末尾 |
| 蓝绿灯 | 蓝灯(constant)无条件常驻;绿灯(selective)按关键词/正则命中才激活 |
| 概率激活 | 条目可按概率抽取,且结果可复现——同一会话同一轮次永远得到同一个判定 |
| 正则替换 | 按作用面(条目正文 / 整段系统提示 / 用户消息)做正则改写,支持捕获组 |
1. 「位置」的含义完全不同
酒馆的位置是相对角色卡的:before/after char definition、AN top/bottom、at depth。它有一套聊天消息数组,投喂点就是其中几个插槽。
DSH 没有角色卡,但它有一个分段装配、有官方锚点序号的系统提示。所以本插件的 layer 直接对齐 @deepseek-ai/dsh-system-prompt 导出的 SECTION_ORDERS 常量——「插到工具指引之后、文件交付引用之前」这种意图可以精确表达,不用猜魔数:
0 harness:identity (-1000)
1 lorebook-prompt:layer-early (-500) before-persona
2 deployment:persona-prefix (0)
3 lorebook-prompt:sys-redact (700)
4 ref:middle (3000)
5 lorebook-prompt:green-never (3150) tool-policy
6 deployment:persona-suffix (10200)
7 lorebook-prompt:layer-late (10300) tail
8 ref:hidden (11000)
(这是 test/integration.mjs 在真实 SystemPrompt 服务上装配出来的实际顺序,不是示意。)
2. 概率必须是确定性的
酒馆是直接获取随机数。本插件不能。
DSH 有一条硬规则:模型可见 ⟺ 已记录。会话日志必须能完整重放模型当时看到的内容。真随机会让重放得到与当初不同的结果,直接违反这条铁律;再叠加上下文缓存按前缀匹配,逐轮抖动的系统提示会把缓存全部打废。
所以概率门用的是 FNV-1a 稳定哈希而不是 Math.random():同一 (条目 id, seedSalt, 会话, 轮次) 永远得到同一个判定。
「概率 30%」在本插件里的准确含义是「按轮次确定性抽取」,不是「每次掷骰子」。 对提示词工程来说这反而更好用:你得到的是可复现、可调试的行为,而不是一个无法复现的噪声源。
3. 多出来的一层:通道(channel)
酒馆只有一个注入面(聊天消息数组)。DSH 有两个:
channel: "system" | channel: "message" | |
|---|---|---|
| 落点 | 系统提示的有序段落 | 转换成 user 角色快照的动态上下文(systemPrompt.context()) |
| 作用域 | 每会话稳定 | 每轮独立 |
| 概率语义 | 按会话抽取(系统提示不逐轮抖动) | 按轮次抽取(可复现但会变) |
| 适合 | 风格约束、工具约定、分层规则 | 关键词触发的临时指令、概率性提醒 |
你要的是「每轮有一定概率追加一段提示」,就用 message 通道。 详见下面的缓存安全一节。
4. 明确被移除的东西
| 酒馆有 | 本插件 | 为什么 |
|---|---|---|
| Token Budget / 预算裁剪 | ❌ 不要 | DSH 自己管系统提示的分段装配与预算。插件再插一层预算分配,会和宿主打架,而且它不知道自己排在第几段之前。 |
| Recursion(条目命中后触发更多条目) | ❌ 不要 | 递归注入会让扫描文本自己污染自己——这是个大坑:用已注入后的消息做扫描源,一条正文里含自身触发词的绿灯条目会永远自我续命。已修,并且有了回归断言。递归会把同一类问题放大到不可审计。 |
| Sticky / Cooldown / Delay | ❌ 不要 | 这些是有状态的时序机制,与「日志可重放」冲突。 |
| Group Scoring / 权重抽签 | ❌ 不要 | 组内抽签本质是随机,同上。 |
| 角色卡、Persona、RP 前端、聊天记忆 | ❌ 不要 | 那是别的插件的职责。见下一节。 |
| Vectorized / 向量检索条目 | ❌ 暂不 | 需要 embedding 服务依赖;不想为了一个可选项把插件从「零依赖纯数据变换」变成「需要外部服务」。 |
生态位空缺
这个生态位不是空的。检索依据:市场自己的目录(awesome-dsh-plugin,npm 包 dsh-plugin-catalog,构建号 2026.1010.4974),共 4471 个插件,按「世界书/lorebook」「系统提示注入」「关键词触发注入」「正则改写」等关键词做了三轮全量检索。所以下面是按公开描述划出的功能范围。
| 插件 | 它做的事 | 和本插件的差别 |
|---|---|---|
| dsh-universal-worldbook ⭐3 | 酒馆风格的通用世界书:请求级一次性提示词注入,兼容酒馆世界书格式,关键词/正则/常驻触发,另带 lore_lookup 查询工具 | 最接近的先例,也是唯一一个把世界书从 RP 里剥出来的。 但它固定走「给最新一条用户消息附加一次性注入」,也就是放弃系统提示分层——那恰好是本插件缓存安全改道的降级行为。它把这当成唯一模式,本插件把它当成兜底。另:无 npm 包,只能从 GitHub 装。 |
| Lume ⭐4 / 3905 dl | 纪律层插件,「约束按需注入,闲聊轮不额外加 token;注入分层(系统段只放恒定内容,易变内容随对话尾部快照下发),前缀缓存每步可用」 | 分层思路和缓存处理与本插件高度一致——这是全部 4471 个插件里唯一在自述里明确提「前缀缓存」的。但它的规则是插件内置的固定纪律,用户不能编排;分层是它的实现细节,不暴露给用户。 |
| dsh-custom-mode ⭐9 / 11228 dl | 每个模式写一份自定义系统提示词,agent loop 每次调用前重读,改完立即生效 | 系统提示注入 + 热重载都重叠。但颗粒度是整份模式级的替换,不支持逐条目开关、层锚点、概率。 |
| dsh-meow-memory ⭐169 / 9262 dl | 七层 SQLite 记忆,「首条消息缓存友好注入 + 每条消息关键词命中」 | 「缓存友好注入 + 关键词命中」两件事都做。但触发源是自动积累的记忆库,不是人手写的规则条目。 |
| dsh-tavern-v2 ⭐43、dsh-nexttavern ⭐156、dsh-LorebookMD ⭐23、InfiniteDSH ⭐7 | 世界书是角色扮演娱乐/小说工作台里的子功能模块 | 世界书是工作台的一个零件,绑死角色卡、预设、会话视图、分支。拿不出来单独用。 |
| dsh-plugin-masterprompt ⭐10、dsh-agent-persona ⭐2 | 整体级人设/提示词注入 | 换一整份人设或按会话分发;没有逐条目模型。 |
| dsh-prompt ⭐29、dsh-composer-ux ⭐3、dsh-prompt-enhance ⭐10 | 输入框侧模板,提示词辅助润色 | 帮你在发送前写提示词,不进注入层,不参与系统提示装配。 |
| dsh-muv-engine ⭐2 | 对模型输出做正则改写 | 正则替换发生在上游——改的是模型已经生成的文本。本插件的 replaces 改的是送进模型之前的提示词。方向相反。 |
| dsh-auto-memory ⭐105、dsh-anchor ⭐2 | 长时记忆/知识库注入 | 注入内容和时机由插件自己的算法决定。 |
缺口到底在哪
把四项核心能力拆开看,每一项在市场上都能找到邻居:
| 能力 | 市场上有对应物吗 |
|---|---|
| 酒馆式蓝绿灯 + 关键词/正则触发 | ✅ 有,但都在 RP 工作台里,或由插件内置规则驱动 |
| 系统提示注入 | ✅ 有,但都是整份人设/模式级别 |
| 概率激活 | ❌ 检索范围内没有用户可编排的概率条目 |
| 层锚点(插进系统提示的指定位置) | ❌ 检索范围内没有对用户暴露的层锚点 |
真正的缺口是三条:
- 没有人把「层锚点 + 蓝绿灯 + 概率 + 正则替换」四件事装进同一份用户可编辑的规则文件。 邻居们各占一项,且都把这四项里的某一项写死在插件内部。
- 没有人把「位置」当成一等公民。
dsh-universal-worldbook干脆放弃分层,全部走尾部。本插件的layer对齐 DSH 官方SECTION_ORDERS,让「插到工具指引之后、交付物引用之前」这种意图可以精确表达。 - 缓存前缀几乎没人在乎。 4471 个插件里只有 Lume 在自述中明确提到前缀缓存。而提示词注入是唯一会动到请求前缀的一类插件——这个风险被普遍忽略了。本插件把「
system+selective这类缓存敌意组合自动改道到消息末尾」做成默认行为,并且从不静默。
一句话:这个位置不是没人碰,是没人把它当成一个独立的、用户可编排的提示词工程层来做。
顺带说明本插件刻意不做的事,避免和市场里的邻居撞车:不注册任何工具(包括不做
lore_lookup),规则完全由人工编排——一个能自助改写注入规则的 Agent 不再可审计,而可审计正是这个东西存在的意义。
安装
dsh plugin --profile add dsh-lorebooklike-prompt-tool
dsh --profile --dump-config # 应出现 "# == dsh-lorebooklike-prompt-tool" 层
`` 换成你实际在用的 profile 名:Electron 桌面应用里是 desktop,Web 场景是 web,
纯命令行场景是 headless。不确定自己有哪些,看 $DSH_HOME/profiles/ 下的目录名即可——
目录名就是 profile 名(node_modules 除外)。
想装仓库里的最新开发版(可能超前于 npm 上已发布的版本),用 pnpm 的 git 源简写直接指向仓库:
dsh plugin --profile add github:erlingmijiang/dsh-lorebooklike-prompt-tool
本地开发时用目录链接:
dsh plugin --profile add ./dsh-lorebooklike-prompt-tool
⚠
desktop(Electron 托管)profile 是个例外,实测有两个坑:
dsh --profile desktop --dump-config会被直接拒绝:profile "desktop" is managed exclusively by the Electron applicationdsh plugin --profile desktop add只会写dependencies,不会把包追加进dsh.profile.bundles(其他 profile 没有这个限制,一条add就到位)所以装完要手动把
"dsh-lorebooklike-prompt-tool"追加到dsh.profile.bundles数组末尾, 或者在市场/插件页里把它的开关打开。验证层合成成功可以换个非托管 profile 做—— 同一个包在那边 dump 会打印# == dsh-lorebooklike-prompt-tool。
热重载:改规则文件即时生效,不需要重启。
它挂在哪些扩展点上
插件只挂在 DSH 已有的、已文档化的扩展点上,不新增任何会话事件类型:
| 挂点 | 负责 |
|---|---|
ctx.systemPrompt.section() | system 通道条目 —— 有序系统提示段,order 对齐官方锚点 |
ctx.systemPrompt.context() | message 通道条目 —— 物化成 user 角色快照的动态上下文 |
agent/pre-step 瀑布 | 记录本轮扫描文本;施加 target: "user" 的正则替换 |
system-prompt/assemble 瀑布 | 施加 target: "system" 的正则替换 |
ctx.inject(['webServer']) | 可选:设置页 HTTP API(作用域分支,不是硬依赖) |
这么选有一个具体好处:会话日志格式完全没变。不需要自定义 SessionEventMap 成员,也就不会出现「旧版本 harness 拒绝加载整份日志」的问题。
设置页
浏览器里 设置 → 世界书提示词,一个类酒馆世界书的编辑器:左侧条目列表(带 🔵蓝/🟢绿、概率、「改道」徽标),右侧编辑全部字段——通道、模式、分层锚点、关键词与副键、概率、正文、正则替换。支持新增/复制/删除条目,改完点保存即写回 rules.json 并立即生效,不用重启。
界面直接改你磁盘上那份文件,不引入第二个数据源。写入的是用户面原始 JSON——effectiveChannel、sortKey、routedForCache 这些引擎算出来的字段只在界面里显示,绝不写回文件,否则它们会固化成声明的一部分,越滚越脏。
设置页接口(
/lorebook-prompt/state、/save、/reload)只接受本机回环地址——它能改提示词注入,不会被局域网里的其他人修改。如果你的 profile 没有 web 服务器(
headless、tui),接口会被跳过但提示词注入照常工作。
缓存安全:为什么绿灯默认不进系统提示
上下文缓存匹配的是请求的前缀。系统提示在请求最前面,所以它一变,后面所有内容都要重算——缓存全废,token 成本成倍上升。
什么会变?只有绿灯(selective)。 它按关键词逐轮开关。蓝灯永远在;概率条目按会话抽取,一个会话内恒定。所以唯一的缓存杀手是:
channel: "system" + mode: "selective" ← 每次翻转都重写请求前缀
默认行为:这类条目会被自动改注入到 message 通道。宿主把 message 通道的内容追加在消息数组末尾(dsh-agent-loop 里是 [...claimed, context]),前面的一切原样保留——这正是「注入在前文下面,不动已有前缀」。
插件加载时会打一条告警列出被改道的条目,状态文件里有 cacheRouted 计数,界面上显示「改道」徽标。从不静默。
想保留原语义的两种方式:
| 方式 | 写法 |
|---|---|
| 全局关闭 | "defaults": { "cacheSafe": false } |
| 单条豁免 | 给该条目加 "cacheUnsafe": true |
实测:集成测试里真实
SystemPrompt装配出的段落序列中,本插件的lorebook-prompt:green-*段一个都不剩——系统提示里不再有任何会逐轮翻转的内容。
怎么确认它在跑
这个插件的全部工作是静默改写提示词——不注册工具、界面上看不出任何变化。所以它在加载、热重载、卸载三个时刻会往规则文件旁边写一份状态:
$DSH_HOME/lorebook-prompt.status.json
{
"plugin": "dsh-lorebooklike-prompt-tool",
"event": "loaded",
"at": "2026-10-08T00:49:48.000Z",
"pid": 1948,
"node": "v24.18.1",
"rulesPath": "/lorebook-prompt.json",
"entries": 8,
"enabled": 6,
"systemSections": 4,
"messageContexts": 2,
"declaredChannel": { "system": 4, "message": 2 },
"byChannel": { "system": 2, "message": 4 },
"cacheSafe": true,
"cacheRouted": 2,
"settingsApi": true,
"settingsApiPath": "/lorebook-prompt",
"migratedFrom": null
}
| 字段 | 含义 |
|---|---|
event | loaded / reloaded / saved / unloaded / error |
detail | 出错时的原因(event: "error" 时才有) |
pid | 写它的进程——能区分是主应用加载的还是别的进程(排障时这一项价值极高) |
migratedFrom | 从旧文件名迁移过来的来源路径;没迁移过是 null |
enabled | 启用条目数 |
systemSections / messageContexts | 实际注册出去的段与上下文数量 |
byChannel | 实际生效的通道分布 |
declaredChannel | 规则文件里声明的通道分布(与上一项不同即说明有条目被改道) |
cacheSafe / cacheRouted | 缓存安全开关,以及被改道的条目数 |
settingsApi | 设置页 API 挂上没有 |
settingsApi 是重启后排障的第一眼,它把三种「设置页不出现」区分开:
| 现象 | 结论 |
|---|---|
| 没有状态文件 | 插件压根没加载(组合层问题) |
settingsApi: false | 这个 profile 没有 webServer——headless/tui 的正常情况 |
settingsApi: true 但页面没出现 | host 半边没问题,问题在客户端半边没被 shell 加载 |
文件不存在 = 插件还没被加载。 这是最直接的一声「我在跑」。
一个真实踩过的坑:
设置页报 404、而
settingsApi是false。根因是 host 半边用ctx.get('webServer')拿服务—— 而 Cordis 里未写进inject的服务,即使活着,ctx.get也返回undefined。于是路由从未注册。 正确写法是ctx.inject(['webServer'], scope => ...):服务出现时激活那个作用域分支,缺席时只是分支不激活。 也不能把它写进顶层inject——那会让headless/tui这类没有 web 服务器的 profile 停在 PENDING, 核心的提示词注入整个失效。两条路都错,各有各的失败方式。更值得记的是为什么当时那 105 项测试没拦住它:测试里的模拟 ctx 中
get(name)返回了服务, 比真实 Cordis 宽容,于是「看起来能用」。比现实宽容的测试替身价值是负的——它给你虚假的安全感。 现在那个 mock 已被改成忠实的(get()恒返回undefined),并且验证过它确实能拦住这类 bug。