Bundle
@dsh-external/dsh-prompt
Editable prompt snippets injected into DSH on three channels: the system prompt, the per-turn runtime context, and a user message at each turn's start. DSH 提示词注入插件:片段可增删改、按通道注入系统提示词 / 运行时上下文 / 每轮用户消息,带设置页与输入框快捷开关。
- Source
- hongweifei
- License
- MIT
- Updated
- Updated 7 days ago
Readme
# @dsh-external/dsh-prompt
DSH 提示词注入插件:把可复用的提示词片段注入到模型的三个位置,并随时增删改。
「一个提示词」其实有三种完全不同的生命期,所以本插件分成三条通道:
| 通道 | 注入位置 | 适合放什么 | 生效范围 |
|---|---|---|---|
| **系统提示词** `system` | 系统提示词里的一个 section | 固定人设与规则 | 每一轮、每个请求 |
| **运行时上下文** `context` | 宿主的 runtime-context 快照(一条 user 角色消息) | 每轮的环境事实 | 每轮刷新 |
| **每轮用户消息** `message` | 每轮开头的一条用户消息 | 临时要求 | 每轮一次 |
三条通道各自可开关。是否注入由两层决定:
- **默认模式**(设置页):出厂为**默认关**。没单独设置过的会话按它执行。
- **会话开关**(输入框旁按钮 / 设置页的「覆盖」):只影响当前会话,`跟随默认` / `强制开` / `强制关`。
**装完不会立刻改变任何请求**——这是刻意的:注入会改写每一次请求,所以它必须由你手动打开。
## 三个入口
- **设置页**:设置 → 「提示词片段」。完整管理:增删改、启停、同通道内上移下移、会话内片段、注入预览、存储路径,以及**默认模式(默认开 / 默认关)**。
- **输入框旁的快捷开关**:对话输入框工具行里的一枚小按钮。一次点击把**当前会话**切到相反状态;当「跟随默认」已经能给出想要的结果时,它会清掉覆盖而不是留下一条多余的记录。鼠标悬停提示「下一次点击会变成什么」。
- **斜杠命令**:无 Web 连接的 profile 也能用。
- `/prompt` — 列出片段与各通道状态(`/prompt on|off` 改默认模式)
- `/prompt-add <名称> | <system|context|message> | <正文>`
- `/prompt-remove <id>`
- `/prompt-session [status|auto|on|off|add <正文>|clear]`
## 变量
片段正文里可以写三个变量,三个通道都支持:
| 变量 | 含义 |
|---|---|
| `{{cwd}}` | 当前会话的工作目录 |
| `{{model}}` | 当前模型 id |
| `{{provider}}` | 当前 provider |
变量取自**正在组装的那次请求所属的 agent**,不是宿主进程的目录 —— 否则面板报的路径和会话实际所在的目录会不一致。
## 两处必须知道的实现约束
1. **系统提示词通道注册为 `interpolate: false`。** 宿主的 `systemPrompt.section` 默认会对文本做 `{{变量}}` 插值,遇到未知变量会**抛错并让整次模型调用失败**。本插件自己做变量替换,然后把字面文本交出去,所以用户写 `{{` 永远不会炸。
2. **运行时上下文通道无法关闭插值**(`PromptContext` 没有这个字段),所以该通道会把每一处 `{{` 改写成 `{ {`,让宿主的扫描器看不到变量组。**这个改写会在注入预览里明确提示**,不会静默发生。
⚠ 这里有个真实踩过的坑:天真的 `replaceAll('{{', '{ {')` **只处理不重叠的成对括号**。用户写 `{{{a}}}` 时它得到 `{ {{a}}}` —— 里面**仍然有一个 `{{a}}` 组**,宿主读到未知变量就抛错,整次模型调用直接失败(现场报错:`unknown prompt variable "{{a}}" in context "prompt:user-snippets"`)。现在的实现是逐字符扫描,无论连续多少个花括号都不会留下 `{{`。`test/interpolate.test.mjs` 把宿主自己的 `interpolate()` 逐字抄来当裁判,用 27 组对抗性文本验证转义结果。
## 配置
放在 profile 的 `cordis.patch.yml` 里,作为 `id: prompt` 覆盖:
```yaml
- id: prompt
name: '@dsh-external/dsh-prompt'
config:
enabled: true
maxChars: 8000 # 三通道合计字符上限;0 = 不限制
sectionOrder: 100 # 系统提示词 section 的位置(0 = persona,500 = plan policy)
contextOrder: 200 # 运行时上下文的位置(宿主自带的是 110/115/120)
refreshMs: 3000 # 存储快照重读间隔;0 = 只靠启动与面板写入刷新
```
## 数据
片段存在 `<DSH_HOME>/prompt/snippets.json`,一个文件,跨项目共享(提示词是偏好,不属于某个项目)。可以直接手改;插件每 3 秒重读一次,保存后立即生效。
写入走宿主自己的 `ctx.fs` 与新鲜度校验:面板保存时会重新读取并**重放在别人写入之后**,不会把并发修改覆盖掉。
## 安装
```bash
# 在 profile 目录里(web 这类 CLI 管理的 profile)
dsh plugin --profile web add "link:<本仓库路径>"
```
desktop 这类由 Electron 管理的 profile 只能用宿主内的 plugin_manager 安装,安装后**需要重启应用**(宿主半边在进程启动时载入)。客户端半边改完只需刷新页面。
## 测试
```bash
node test/run.mjs # 全部:单元 43 + 插值安全 6 + 集成 41 + 客户端 22 + 架构 12 = 124
node test/check-encoding.mjs # 全树扫描 CP936 损坏与 BOM(提交前必跑)
node test/preview.mjs # 渲染真实面板 → test/preview.html
node test/shots.mjs # 截图到 test/shots/(zh/en × light/dark)
node test/probe-live.mjs # 用真实存储回读三个通道的最终文本(排查现场用)
node test/probe-session.mjs <会话目录> # 逐帧解压会话日志,查某段文本是否真的注入过
```
`test/shots/*.png` 是随仓库提交的文档,改面板就要重新生成;`shots.mjs` 会比较四张图的哈希,**两张图字节相同就直接报错**(Edge 会按 URL 缓存 `file://`,否则第二次截图会静默复用第一张)。
### ⚠ 不要用 PowerShell 改本仓库的文本文件
这台机器的 PowerShell 5.1 是 CP936 代码页:`Get-Content -Raw` + `Set-Content` / `WriteAllText` 的往返会把这个 UTF-8 文件按 CP936 解码再按 UTF-8 写出,**每一个非 ASCII 字符都会变成乱码,且部分字节直接丢失、不可还原**(`—` 会变成 `?`)。本项目因此损坏过 `lib/client.js` 两次、`test/unit.test.mjs` 一次。
用 read/edit/write 工具,或写一个 Node 脚本(Node 的 `fs` 严格按 UTF-8 读写)。提交前跑 `node test/check-encoding.mjs`——架构测试里也有一道同样的守卫。
## 许可
MIT
Install
dsh plugin --profile web add github:hongweifei/dsh-prompt
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-external-dsh-prompt from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.