dsh-prompt-injector
Manifest valid★ 1DeepSeek Harness plugin: inject a reusable prompt preset into the front of a message at send time - next message only, or every turn. Prompt injector
English | 简体中文
dsh-prompt-injector · 提示词注入器
一个 DeepSeek Harness Web 插件:把可复用的提示词预设在发送的那一刻注入到消息首部,而不是填进输入框。
注入几次由你决定:💌 只注入下一条(默认)或 💖 每轮都注入。
萌系二次元画面风格:🌸 粉紫渐变、圆角卡片、会呼吸的小表情。
在 DeepSeek Harness 0.2.0-rc.2 上实测可用(Host 侧 67 个用例,浏览器半在真实 GUI 里跑通,见已验证)。
长什么样
先选注入方式,再点预设:

选好之后 chip 变成 💌 名字 下一条,输入框里一个字符都不会多:

发送时才是注入发生的时刻。下面是「每轮都注入」连发两条的结果——两条消息都带上了预设块,而输入框始终只有你自己写的话:

和「提示词预设」类插件的区别
| 常见的预设插件 | 提示词注入器 | |
|---|---|---|
| 选中预设后 | 把提示词填进输入框 | 只在输入框左侧显示一个 💌 / 💖 徽章,输入框内容完全不动 |
| 你打字时 | 要在一大段提示词后面接着写 | 输入框里只有你自己写的话 |
| 注入几条 | 选中就一直生效 | 默认只注入下一条,发完自动松开;想每轮都注入得自己切 |
| 发送时 | 提示词就是消息的一部分(你在输入框里看得见) | Host 端在 agent/pre-step 把提示词拼到消息最前面 |
| 对话记录 | 提示词和你的话混在一起 | 记录里是完整的注入后文本(模型看到什么,记录就是什么) |
实现位置:lib/inject.js 的 applyInjection(),注册在 agent/pre-step 瀑布事件上——这是消息进入会话日志之前的最后一个接缝。
为什么不是「往输入框塞标记」
最初的设计是往 composer 草稿里写一个不可见的选取标记,随消息一起送到 Host。实测下来这条路在当前版本走不通:
- DSH 的 composer 是 Lexical 编辑器,没有任何公开的草稿写入服务;
- Lexical 自己拥有文档状态,直接改 DOM 会被它下一次 reconcile 覆盖;
- 通过 DOM 事件(
execCommand('insertText'))写入也依赖当前选区,而打开面板后焦点已经不在编辑器上。
所以选择改由 Host 侧按会话记录:浏览器半把「这个会话选了哪个预设、注入到什么时候」通过插件的 HTTP 接口告诉 Host(lib/http.js 的 /selection),Host 在发送瞬间查表注入。这样完全不依赖编辑器的内部实现,也不需要污染消息文本——顺带还让「只注入一条」变得容易实现:Host 注入完就把这条选择丢掉。
早期版本还写了一套「尽力而为」的草稿写入代码,已经删掉了:它写得进去也不起决定作用(真正生效的是 Host 侧的选择),留着只会让「到底谁说了算」变模糊。现在插件完全不碰输入框。
安装
插件是「bundle」形态(根目录有 cordis.patch.yml),用 dsh plugin 安装:
dsh plugin --profile web add "github:yukk4159-bit/dsh-prompt-injector"
dsh plugin 把包管理转发给 pnpm,所以也可以装本地路径或指定分支:
# 本地检出(改代码时用这个,改完重启就生效)
dsh plugin --profile web add "/path/to/prompt-injector"
# 指定分支或 tag
dsh plugin --profile web add "github:yukk4159-bit/dsh-prompt-injector#main"
或者在没有全局 dsh 的环境里:
npx -p @deepseek-ai/dsh dsh plugin --profile web add "github:yukk4159-bit/dsh-prompt-injector"
装完重启该 profile,然后打开 Web GUI:
- 输入框左边会多一个 🌸 预设 按钮 —— 点开挑预设;
- 左侧边栏 → 设置 → 提示词注入器 —— 管理预设库(增删改查、搜索、标签过滤)。
用法
- 设置页写预设:名字、ID(可选)、标签、注入内容 → 保存。
保存会写进该 profile 的配置文件(
cordis.patch.yml的config.presets),重启、切会话都不会丢。 - 在输入框左边点 🌸,面板顶部先选注入方式,再点一个预设:
- 💌 只注入下一条(默认)——只有你紧接着发送的那一条会被注入,Host 注入完就把这条选择丢掉,再发就干干净净;
- 💖 每轮都注入——一直生效,直到你点「取消」。
面板顶部的切换是立刻生效的:已经选了预设时改方式,等于同时把那条选择改成新方式。
chip 上会写清楚现在是哪种(
💌 名字 下一条/💖 名字 每轮),不会出现「以为只注入一次、其实一直在注入」。
- 正常写你的话,回车发送。
- 发送瞬间,Host 把预设正文按模板拼到消息最前面再交给模型。
- 默认方式下,发完那一条之后 chip 自己回到 🌸(面板打开时每 3 秒问一次 Host,因为一次性选择是 Host 释放的,它没法主动通知页面)。
再点一次同一个预设、或点面板里的「取消」即取消。
不点面板也能用:{{ID}}
在消息里直接写 {{code}}(code 是这个预设的 ID),那一条就会用对应预设,发完即止——这本来就是一次性的,和 chip 的选择互不干扰。
- 写了一个库里有的 ID:注入并把这处
{{code}}从消息里拿走; - 写了一个库里没有的 ID:当成普通文字原样发出去,也不会挡住 chip 的选择;
- 指向已停用的预设:不注入,文字原样保留。
注入取值的优先级
- 消息正文里的选取标记(
{{id}},或早期版本写入的私有区标记); - 本会话的选择(就是上面第 2 步记录的那个);
config.defaultPresetId。
私有区标记若指向一个不存在或已停用的预设,会刻意压掉后两级回退——你点名要它,就不该偷偷换成别的。手打的 {{id}} 没这个特权,因为它可能只是普通的文字。
关系
- 选择是按会话的:不同会话可以选不同预设,互不干扰。
- 「注入几条」记在浏览器里(
localStorage),下次打开面板还是你上次选的方式。 - 选择本身存在 Host 进程内存里,重启即清空——它表达的是「这一轮我想用哪个」,不是持久配置。想常驻就用
defaultPresetId(它按定义就是每轮都注入)。
预设长什么样
presets:
- id: code
name: 代码评审
text: 请以资深工程师视角严格评审这段代码,指出风险与改进点。
tags: [work, code]
enabled: true
注入模板
config.template 决定包装形式,支持 {{name}}、{{id}}、{{text}}:
[{{name}}]
{{text}}
[/{{name}}]
配 config.separator(默认两个换行)拼在你自己的文字和预设块之间。
config.position 从 prefix(默认,消息首部)改成 suffix 就改成拼到末尾。
全部配置项
| 配置 | 默认 | 说明 |
|---|---|---|
enabled | true | 总开关。关掉后 UI 还在,但不再注入。 |
position | prefix | prefix 拼到消息首部,suffix 拼到末尾。 |
template | [{{name}}]\n{{text}}\n[/{{name}}] | 预设正文外面的包装。 |
separator | 两个换行 | 预设块与你自己的文字之间的连接串。 |
defaultPresetId | 空 | 兜底预设:没人选、消息里也没有标记时用它。它按定义是每轮都注入;留空表示不注入。 |
agentPresets | [] | 只对这些 agent preset 生效;留空表示全部。 |
libraryPath | 空 | 可选的 JSON 副本路径(默认 <DSH_HOME>/prompt-injector/prompt-injector.json)。 |
presets | [] | 预设库本体。 |
「注入几条」不是配置项:它是每次挑选时的一个手势,所以留在界面和浏览器里,不写进配置。
数据存在哪
- 预设库主存储:该 profile 的
cordis.patch.yml里的config.presets。 设置页保存时通过 Harness 的settings.update()写回,所以它同时是「插件配置」和「预设库」,设置页的自动表单也能直接编辑它。 - 可选 JSON 副本:设置在
settings提供方不可用时(或者你想让库跟着仓库走)会退化写到libraryPath;用一个 profile 里没有的 agent 跑时也能从这里读起来。 - 会话选择:只在 Host 进程内存里,不落盘(见上)。
HTTP 接口
浏览器半用这三条路由和 Host 半对话;它们挂在组合后的 Host Web 服务器上,因此同样受该服务器的浏览器信任栅栏与鉴权保护。
| 方法 | 路径 | 作用 |
|---|---|---|
GET | /prompt-injector/prompt-injector/library | 读预设库、标签和全部会话选择 |
PUT | /prompt-injector/prompt-injector/library | 整库覆盖写入 |
PUT | /prompt-injector/prompt-injector/preset | 新增或更新一条(按 id) |
DELETE | /prompt-injector/prompt-injector/preset?id=… | 删除一条 |
GET | /prompt-injector/prompt-injector/selection | 只读会话选择(面板的轮询用这条) |
PUT | /prompt-injector/prompt-injector/selection | 记录/清空某会话的选择,body 为 {sessionId, presetId, mode},mode 取 once / sticky |
GET /library 返回的 selections 形如 {"session-…": {"presetId": "code", "mode": "once"}}。当 mode 是 once 时,Host 注入完那一条就会把这一项删掉——所以下一次 GET 里它就没了,面板据此把 chip 还原成 🌸。
路径里的 prompt-injector 是插件条目 id,两个实例不会互相覆盖。
项目结构
prompt-injector/
├─ cordis.patch.yml # bundle 入口:一行 insert
├─ package.json # dsh.bundle + dsh.client 声明
├─ lib/ # Host 半(Node)
│ ├─ index.js # 插件入口:Config、pre-step 注入、一次性选择的释放、路由与存储装配
│ ├─ config.js # Config schema(单独一个文件,为了隔离 schemastery 依赖)
│ ├─ core.js # 纯函数:预设规范化、标记编解码、注入方式、模板渲染、拼接
│ ├─ inject.js # agent/pre-step 的注入逻辑(纯函数 + 消息重写)
│ ├─ store.js # 预设库存储 + 会话选择(含 once/sticky):settings 写透 + JSON 原子副本
│ ├─ http.js # /prompt-injector/<id>/library、/preset、/selection
│ └─ client/client.js # 构建产物:浏览器半
├─ client/ # 浏览器半(手写 lazy-CJS 工厂,无需打包器)
│ ├─ index.js # 入口:apply/inject、槽位注册、库与选择同步、方式偏好
│ └─ src/ # config / core / styles / composer / settings-page
├─ scripts/build-client.mjs # 把 client/ 组装成 client/client.js
├─ docs/ # README 里的截图
└─ test/ # node:test,无第三方依赖
client/client.js 是提交进仓库的构建产物:Harness 直接加载它,所以别人 clone 下来不需要构建就能装。
构建浏览器半
node scripts/build-client.mjs
脚本自己做四件事:解析相对的 import、把 react / react-dom 转成平台 require、把每个源文件包进各自的闭包、最后包成模块加载器要的
window.__ModuleLoader__.load({ id, factory })。没有打包器依赖。
测试
node --test --experimental-test-isolation=none "test/*.test.js"
# 或
node scripts/build-client.mjs && node --test --experimental-test-isolation=none "test/*.test.js"
必须写文件通配符:--experimental-test-isolation=none 下不能直接传目录。
在受限沙箱(workspace-write)里也必须保留 --experimental-test-isolation=none——默认的测试运行器会用管道 spawn 子进程,被沙箱拒绝(EPERM)。
覆盖:预设规范化 / 标记编解码(含手打 {{id}})/ 注入方式归一化 / 模板与拼接 / 消息重写(含非人类消息的排除、禁用预设、
allow-list、{{id}} 命中与否)/ 会话选择与一次性释放 / 存储写透与 JSON 原子副本 /
浏览器 bundle 的注册与槽位贡献 / Host 端 apply() 的完整 pre-step 路径。
共 67 个用例;其中「Config 默认值」需要 @deepseek-ai/schemastery 可解析,在纯仓库检出里会跳过。
已验证 / 待验证
已经实测:
- 注入真的发生在发送时、并且真的到了模型请求里。用一个临时观察插件监听
llm/stream,对agent/pre-step改写后的消息取证:请求里拿到的是[PROBE-INJECTED] Reply with exactly: PROBE-OK,说明改写后的内容就是模型收到的那一条。 - 插件能被真实 profile 加载:
dsh plugin --profile <name> add <本目录>之后 bundle 层出现- id: prompt-injector,Host 半正常 apply,禁用预设被过滤,HTTP 路由注册成功。 - 浏览器半在真实 GUI 里跑通:输入框左侧出现 🌸 预设 chip;面板能打开、搜索、按标签过滤;选中后 chip 显示预设名与方式;Host 侧
/library的selections里能看到该会话的选择;「取消」后选择被清空。 - 「只注入下一条」真的只注入一条:在预览 profile 里真的打字 + 回车发送 4 条消息(该 profile 用的是假密钥,请求会 401,但注入发生在
agent/pre-step,即请求之前)。Host trace 与写进会话日志的正文分别是:- 第 1 条(💌 选
code-review):changed=true preset=code-review source=session - 第 2 条:
changed=false source=none—— 选择已被释放 - 第 3 条(切到 💖
novel):changed=true preset=novel source=session - 第 4 条:
changed=true preset=novel source=session—— 每轮模式下继续注入 会话日志里逐条对应:第 1 条带[严格代码评审]…块,第 2 条原样,第 3、4 条都带[小说续写]…块。发完之后GET /selection返回{},chip 自己回到 🌸。
- 第 1 条(💌 选
- 手打
{{id}}也对:发一条{{code-review}} 顺手看看 {{nope}} 这个写法,日志里记下的是预设块 +顺手看看 {{nope}} 这个写法——命中的被注入并摘掉,没命中的原样留着。 - 设置页能读能写:左侧「设置 → 提示词注入器」列出 6 条预设(含停用项,标 🚫),搜索与标签过滤正常,保存后 Host 的
/library确实返回了改动后的库。 - 选择 → 注入的完整链路(自动化用例):
test/host.test.js里一条测试直接走 HTTP 路由写选择,再驱动agent/pre-step,断言消息首部出现[小说续写],并断言 sticky 会连续两条都注入、once 只注入第一条且随后被释放。 - Host 端 67 个用例,66 通过、1 跳过(跳过的那条需要本机可解析
@deepseek-ai/schemastery)。
没验证的:真机上模型真的回话(预览 profile 没有可用密钥)、以及 DeepSeek 官方路由以外 provider 的行为——这两件都和注入逻辑无关。
调试开关
DSH_PROMPT_INJECTOR_TRACE=1 会在 stderr 打出 Host 半每一步(apply、库大小、webServer 等待、pre-step 决策、注入结果)。
实现要点
- 注入点是
agent/pre-step。它以 waterfall 方式运行,本插件先调用next(),再改写返回的messages,因此能和压缩等其它 pre-step 策略组合,而不是覆盖它们。 - 只改写「人类写的」用户消息:
source.kind === 'user'。运行上下文快照、技能提醒、工具追加都不会被注入。 - 每个 step 最多改写一条,避免同一个预设被乘上多份。
- 标记用私有区字符,正常文本里不可能出现,因此不会误伤。
- 失败即放行:任何注入异常都会被记录并原样放行消息,绝不因为插件问题丢失你发出去的内容。
- 资源都有归属:路由、效果、样式表都注册在插件 fiber 的 effect 上,插件卸载自动回收。
已知限制
- 选择只在 Host 进程内存里,重启 DSH 会清空。它是「这一轮用哪个」的意图,不是持久配置;想常驻请配
defaultPresetId。 - 预设库的写入依赖 Harness 的
settings服务。该服务不可用时退化为 JSON 副本,此时设置页的修改只在当前进程内生效。 - 注入后的文本会进入会话日志——这是有意为之:记录应当等于模型真正收到的东西。
- 一次性选择由 Host 释放,页面只能事后知道:面板打开时每 3 秒
GET /selection一次(只在有一次性选择待发时轮询)。所以发完消息后 chip 可能需要最多 3 秒才从 💌 变回 🌸——这不影响注入,只是徽章慢一拍。 {{id}}只在 ID 命中库里的预设时才当作引用,所以预设 ID 起得越像普通词(例如code),越有可能误伤正文里的{{code}}写法。介意的话把 ID 起得独特一点(例如pi-code)。
贡献
欢迎开 issue 报 bug、提想法,或者直接 PR。改浏览器半的代码之后记得跑一次
node scripts/build-client.mjs——client/client.js 是要一起提交的构建产物。
许可
MIT,见 LICENSE。
Comments
Loading…
From the same category
by awesome-dsh-plugin
A curated list of plugins for DeepSeek Harness (dsh) · DeepSeek Harness 插件精选列表
★ 18.4k
CC0-1.0
Python
Oct 11, 2026
by 0xsline
DeepSeek Harness (DSH) ecosystem: curated plugins, tools, and infrastructure from dsh-external/hub and the public dsh-plugin topic.
★ 1.2k
CC0-1.0
Python
Oct 10, 2026
by pax-beehive
Open-source CLI, schemas, resolver, and DSH agent tools for DSH Plugin Hub
★ 406
MIT
TypeScript
Oct 6, 2026
by xiajiajun516
DeepSeek Harness (DSH) backup & restore plugin — export, import, migrate and sync your complete DSH configuration, plugins, MCP servers, skills and workspace. One-click migration to another machine.
★ 177
MIT
TypeScript
Oct 11, 2026
dsh plugin --profile web add dsh-config-managerby yjh051108
推荐组件(非必须):DeepSeek Harness 运行时注入器;已随 dsh-routing-suite 单仓库化保留,本仓库继续维护/发布。
★ 165
TypeScript
Sep 18, 2026
dsh plugin --profile web add @dsh-external/dsh-super-injectorby jigjoy-ai
A CLI that turns a goal into a pull request - and a sandbox for testing concurrent AI coding agents on the Mozaik runtime.
★ 124
MIT
TypeScript
Oct 2, 2026