ILoveHAKU/dsh-dialog-fence

dsh-dialog-fence

给每个对话加一道行为围栏:规则存在一个 JSON 文件里,可在设置页或对话面板里编辑,作为一段固定顺序的运行时上下文注入,不重写系统提示词前缀。

미분류context-engineeringdeepseek-harnessdsh

dsh-dialog-fence

给 DSH(DeepSeek Harness)的每一次对话设定边界与围栏:一套随时增删、下一轮即生效的行为约束规则。
全部规则存在一个 JSON 文件里——用任何编辑器直接改 JSON,或者直接在设置页里逐条编辑。
文件放在哪可以自己定,设置页一键「打开文件夹」就能看到它。
不破坏前缀缓存 · 不膨胀上下文 · 不影响其它对话 · 与 billion-context 压缩共存。

两层规则,叠加注入:

  • 通用规则(fence.json 里的 rules 数组)—— 对每一个对话都生效,包括新开的对话。设置页里主要就是编辑这一层。
  • 会话规则(fence.json 里的 sessions..rules)—— 只叠加到某一个对话,可选,通常在会话右上角的「围栏」浮层里维护。

适用形态:`dsh` CLI(web / headless / acp 等 profile) 与 DSH Desktop。

它解决什么问题

大模型每轮都会忘掉「这次对话的规矩」。常见的补丁方式都有代价:

做法 · 代价 把规矩写进系统提示词 · 内容一变,系统提示词就变 → 前缀缓存整体失效(本机 workbuddy / opencode-go 路由未声明 systemPromptUpdate: 'in-history',会触发 head 重写) 每轮追加一条 user 消息 · 纯追加安全,但永不回收,几十轮后吃掉几千 token 用压缩插件的「规则」功能 · billion-context 0.1.202 的 acp_rule 只存不注(源码里 RULES_USAGE_PROMPT 有定义零引用),plugin 模式 compress.rules 亦不生效

dialog-fence 的做法:把规则注入到 运行时上下文快照(runtime context) 这一段里。

<围栏位置>/fence.json                  ← 单文件:{ enabled, rules: [...], sessions: { <id>: { rules: [...] } } }
   └─ systemPrompt.context({ name:'dialog-fence:rules', order:118 })
        └─ 通用规则 + 启用会话的专属规则 → 拼成一段文本,全都没有就返回空串
             └─ agent-loop: joinContextSections() → "Current runtime context. ..."
                  └─ RuntimeContextProjection.project()
                       ├─ 文本与上次相同  → return(一条都不追加,零 token)
                       └─ 文本变化        → 追加一条新 user 消息(纯 append)

三件事因此同时成立:

  1. 零缓存破坏:从不碰系统提示词;历史里只有「追加」,没有任何前缀被重写。
  2. 零无谓消耗:文本不变时快照不落盘,同一条规则不会每轮重发。
  3. 压缩后仍存活:快照头是 Current runtime context,被 billion-context 的 dshDurableToolPath 归为 durable/runtime-context,自动进 protectedLatestTools(只保护最新一份,旧快照可被压缩),围栏常驻且不膨胀。

隔离性:通用规则为空、且当前会话没有启用专属规则时渲染空字符串,而 renderContextSections() 会 .filter(s => s.text.length > 0) —— 空串 = 零 token、零注入、零副作用。(总开关关掉时,两层一起静音。)

五处界面

界面 · 位置 · 能力 设置页 · 设置左栏 → 「对话围栏」(与「通知提醒」「模型」同级) · 主入口:总开关 + 通用规则全文的可视化编辑(逐条多行编辑框,可换行)+ 新增/删除/清空 + 某个对话的专属规则(下拉选对话,默认选中当前对话;逐条编辑/新增/清空;可单独停用该对话)+ 打开文件夹 + 更改围栏文件位置 插件详情页配置区 · 左侧「插件」→ 已安装 → dsh-dialog-fence → 详情页(包描述与行之间) · 同一份配置界面 会话按钮 · 会话右上角工具区「围栏」,带规则条数徽标 · 打开本会话规则浮层 规则浮层 · 右下角面板(点击面板外部自动关闭) · 与设置页同一套四张卡片(紧凑版):总开关 + 通用规则编辑 + 任意对话的专属规则(下拉默认停在该对话) + 打开文件夹;另有顶部「关闭」 插件页标签页 · 设置 → 内置插件 → 「对话围栏」一页 · 同一份配置界面(老入口,仍在)

五处界面写的是同一个 JSON 文件:面板改完 → 文件落盘 → 下一轮装配时重新读取 → 生效。没有任何状态分叉。 会话按钮上的数字就是真实注入条数(通用 + 本对话专属),不是只数会话层。

设置页的对话下拉里,选项文字用对话名称(重命名过就用你起的名字,否则用项目目录名; 连这些都没有的空白对话显示「未命名对话 N」)——任何情况下都不会把 `session-` 显示出来。 当前对话会标注「(当前)」并默认选中,所以「给现在这个对话加一条专属规则」不需要先选。

为什么卡片在「已安装」而不是「官方」:plugins.item 槽位的语义被写死为「官方分组里的一张卡片」,
改 id / order 都改不到别处;plugins.bundle.config 才属于「组合包自己的配置」,
以 npm 包名 为 key 渲染在该 bundle 详情页的描述与行之间。
0.5.0 起本插件按组合包安装(profile dependencies + dsh.profile.bundles + dsh.bundle.patch 三件齐备),
因此它出现在「已安装」分组,装与卸都走插件页右上角「添加插件」那套官方流程。

安装

推荐走官方插件安装器(装在「已安装」分组里,可一键卸载)。改完必须完全退出并重启 DSH(宿主半边不走 dsh-hmr 热重载)。

方式 A:官方安装器(推荐,出现在「已安装」分组)

包内已带 dsh.bundle.patch: ./cordis.patch.yml、exports、icon 与 dsh.client 声明,三件套齐备(dependencies + dsh.profile.bundles + bundle patch),因此官方安装器直接认它。

图形界面:左侧「插件」→ 右上角「添加插件」→ 在输入框填本包的绝对路径:

C:\Users\<你>\AppData\Roaming\dsh-desktop\harness\dialog-fence

点安装。成功后卡片出现在「已安装」分组(不是「官方」分组),点进去的详情页里就有围栏配置区。 卸载同理:进该包详情页点「卸载」,卡片消失、loader 行一并移除。

命令行:

dsh plugin --profile web add "C:\Users\<你>\AppData\Roaming\dsh-desktop\harness\dialog-fence"

dsh plugin add 装完后,dsh-plugin-manager 的 reconcile() 会自动把声明了 dsh.bundle 的新依赖追加进 profile 的 dsh.profile.bundles 并保存 manifest —— 不需要手工改 profiles//package.json。

⚠️ 重要:本地路径安装是「复制」,不是「链接」
DSH Desktop 的安装后端(profileBundlePackageBackend)会把整个源码目录复制
到 generation 快照里,再在那儿 pnpm add:
`
harness/profiles/.generations/live/dsh-dialog-fence++/node_modules/dsh-dialog-fence/
`
所以改完 `harness/dialog-fence/` 里的源码,已安装的那份不会跟着变,
必须在插件页卸载再装一次(或重跑上面那条命令)。
规则数据不在这里:默认数据目录固定为 /dialog-fence/rules,
与 generation 快照无关,升级换代不会丢规则(见下面「直接编辑规则文件」)。
想要「改一行就生效」的开发体验,用方式 B。

方式 B:file:/// 挂载(开发用,不进「已安装」清单)

# 装到默认 profile(本机 DSH Desktop 用的 profile 叫 web)
node scripts/install.mjs install

# 指定 profile / DSH_HOME;--dry-run 只查看不落盘
node scripts/install.mjs install --profile headless --home "C:\Users\你\AppData\Roaming\dsh-desktop\harness" --dry-run

# 卸载(只删插入行,规则文件保留)
node scripts/install.mjs uninstall

脚本往 /profiles//cordis.patch.yml 末尾追加一段带标记的 insert 块,改写前自动备份成 cordis.patch.yml..bak:

# >>> dsh-dialog-fence (managed by scripts/install.mjs — do not edit by hand) >>>
- insert:
    - id: dialog-fence
      name: "file:///C:/.../dialog-fence/index.js"
# <<< dsh-dialog-fence <<<
若本机没有 node(DSH Desktop 自带的是 Electron),用:
$env:ELECTRON_RUN_AS_NODE="1"; & "D:\DSH Desktop\DSH Desktop.exe" scripts\install.mjs status

这种方式是引用源码目录,改 client.js 刷新页面即可、改 index.js 重启即可,无需重装。 代价:file:/// 插入行不属于 profile bundle,因此

  • 不会出现在插件页「已安装」分组(插件管理清单只列 profile 里真实安装的包);
  • Desktop 日志可能出现 [desktop] profile inconsistency: the patch layer inserts file:///…, which is not installed——只是提示,不影响加载,同一份日志下一行就是 [dialog-fence] ready。
两种方式不要同时用。同时装会有两条 loader 行重复注册槽位。
从方式 B 切到方式 A 之前,先 node scripts/install.mjs uninstall。

验证安装

# 1) 无宿主测试(语法 + 行为,不会碰线上规则目录)
node test/smoke.mjs && node test/route.mjs && node test/client.mjs && node test/render.mjs && node test/opensource.mjs

# 2) 重启后在任意会话里
/fence status

重启并开启围栏后,下一轮的运行时上下文里会出现:

Current runtime context. This snapshot supersedes earlier runtime-context snapshots.

...(沙箱策略 / 审批策略)...

Dialog fence (user-set rules binding this conversation; a newer direct user instruction wins on conflict):
1. 你的第一条规则

看到这段就说明生效了。

用法

可视化面板(web / DSH Desktop)

重启后:

  • 设置左栏 → 「对话围栏」:主入口,直接编辑通用规则(对每个对话生效)。每条占一行:序号 + 正文 + 「改」「删」;点「改」就地变成多行编辑框(0.9.0 起,可换行),Ctrl+Enter 保存 / Esc 取消。底部编辑框新建一条,另有「清空全部」,右侧实时显示 注入 N 字符 ≈ M token。顶部还有 「打开文件夹」 与 「更改位置」。下半部分还有 「某个对话的额外规则」:下拉里按名字选对话(默认已选中当前对话),下面就是那个对话的专属规则列表与新建框 —— 和浮层下半部分是同一份数据、同一套操作(0.10.0 起)。这一块是两行:第一行「对话」+ 下拉,第二行「停用/启用该对话」按钮 + N/40 条专属规则 计数(0.12.1 起,此前挤在一行会把中文压成逐字换行)。
  • 会话右上角出现「围栏」按钮,数字是真实注入条数(通用 + 本对话专属;高亮 = 已启用)。点开浮层,里面渲染的就是设置页那四张卡片(紧凑版式),只是外面套了一层标题栏与「关闭」;下拉默认停在「点开它的这条对话」上,所以浮层里同样能给任意一条对话配专属规则(0.12.0 起)。点浮层外部任意位置即可关闭(0.10.0 起)。
  • 两处都能单独停用某一条对话的围栏(设置页的下拉旁也有「停用该对话 / 启用该对话」,0.12.0 起此前只有浮层有)。
  • 所有改动下一轮生效,无需重启。

三处配置入口(渲染的是同一个组件,内容和行为完全一致):

  • 设置左栏 → 「对话围栏」:一级菜单项,和「通知提醒」「模型」并排。
  • 左侧「插件」→ 已安装 → dsh-dialog-fence:点进该包详情页,配置区在包描述与行之间。
  • 设置 → 内置插件 → 「对话围栏」标签页:老入口,保留。

三处都有:

  • 总开关(关掉后所有对话立即零注入,规则文件原封不动)
  • 通用规则的完整可视编辑(新增 / 改 / 删 / 清空)
  • 某个对话的专属规则的完整可视编辑:下拉选对话 → 该对话的规则逐条「改」「删」→ 底部新建框 → 「添加专属规则」/「清空该对话的专属规则」(0.10.0 起,与浮层下半部分一致)
  • 围栏文件当前的绝对路径 + 「打开文件夹」按钮(在资源管理器里选中它)
  • 「更改位置」:填一个新的绝对路径(或以 .json 结尾的目录)即可把围栏文件搬过去;随后原目录留一个 location.json 指针,删掉它就回到默认位置

面板与这三处配置页都只在 Web 形态出现(CLI / TUI / headless 没有 connection 服务,插件会自动跳过路由注册,/fence 命令照常可用)。

斜杠命令(所有形态)

命令 · 作用 /fence · 查看状态(启用与否、条数、实际注入文本、token 估算、围栏文件路径) /fence on / /fence off · 启用 / 停用本会话的专属规则(不影响通用规则) /fence add · 追加一条本会话专属规则(自动启用;重复文本不会重复添加) /fence rm · 删除本会话第 N 条 /fence clear · 清空本会话规则(保留启用状态) /fence reset · 彻底删除本会话记录(等于从未配置过) /fence dir · 打印围栏文件路径(含位置指针) /fence open · 在资源管理器里打开并选中围栏文件 /fence file · 更改围栏文件位置(现有规则一起搬过去) /fence switch on / switch off · 通用总开关 `/fence all` · 查看通用规则(对每个对话都生效) `/fence all add ` · 追加一条通用规则 `/fence all rm ` · 删除第 N 条通用规则 `/fence all clear` · 清空通用规则 /fence help · 帮助

直接编辑围栏文件(推荐给「随时配置和维护」)

默认位置 /dialog-fence/fence.json(可用环境变量 DIALOG_FENCE_FILE 直接指定文件,或 DIALOG_FENCE_HOME 指定基目录;也可以在设置页「更改位置」)。同目录下的 location.json 是指针文件,{ "file": "" }。

{
  "version": 3,
  "enabled": true,                       // 总开关;false = 所有对话零注入
  "rules": [                             // 通用规则:对每个对话都生效,数组顺序即注入顺序
    "结论必须给出可复现证据(命令 / 文件:行号 / 实测输出三者至少其一);给不出就明说「未验证」。",
    "改文件前先读该文件;改配置或数据前先备份,并给出精确回滚路径。"
  ],
  "sessions": {                          // 会话专属规则:只叠加到该会话
    "session-<uuid>": {
      "enabled": true,
      "rules": ["本对话额外的一条规则"]
    }
  }
}

注入顺序:先 rules 数组 1..N,再把该会话启用的 sessions..rules 接在后面编号。总开关 enabled: false 时两层一起静音。

  • 直接改 `rules` 数组 → 所有对话下一轮生效(包括新开的),无需任何开关登记。
  • 直接改 `sessions..rules` → 该会话自动启用并生效(即使没写 enabled)。
  • 手改坏了 JSON → 不抛异常、不清空文件,本轮降级为零注入,控制台留一条 warn。修好文件即恢复。
  • 上限:每层最多 40 条;渲染总上限 2400 字符;单条上限 2000 字符。
0.3.0–0.6.x 的「一条规则一个 txt」形态(rules/common/*.txt、rules/sessions//*.txt)会在首次启动时自动迁移成单个 fence.json,老文件原样保留不删。
为什么围栏文件默认不放在包目录里:0.5.0 起本插件可以作为组合包被官方安装器装进
profiles/.generations/live//node_modules/dsh-dialog-fence/,那个目录每次升级