zqh260619/dsh-dupguard

dsh-dupguard

同一字符串连续重复达到阈值(默认 10 次)时截停本次生成,已生成内容照常提交;截停后弹出通知,显示所属工作区、会话名称与重复的字符串。重复次数可按单元长度用分段表或 JS 模块决定,代码区域(围栏 / 行内 / 缩进)使用放宽的阈值。

待分类dsh-pluginsrepetition-detection

dupguard · DSH 大模型重复输出守卫

dupguard 是 DeepSeek Harness (DSH) 的实时重复输出守卫插件:最新输出中同一字符串连续重复 10 次及以上(可配置)时立即停止本次生成,已生成内容正常提交为助手消息。

徽章由 npm / GitHub / dsh-plugin.org / dsh.so / awesome-dsh-plugin 各自生成;本插件的版本-兼容矩阵见 兼容性报告。问题与建议请提到 GitHub Issues。

截图

截停通知(可选择是否继续) · 被截停的那一轮:输出停在阈值处 ·

设置页:重复次数(分段表模式) · 分段表:按单元长度决定次数 ·

设置页:忽略白名单与截停通知 · 高级模式(实验性)风险确认 ·

系统通知(可选,Windows 右下角) · ·

7 张图的展示顺序同时声明在 screenshots.json(供插件市场 / 商店读取);截图中的工作区路径已打码。
第 7 张取自桌面版(Electron),通知署名是应用本身;网页版的通知由浏览器转发,署名是浏览器。

快速开始

前置要求:DSH ≥ 0.1.2-rc.1(一条命令安装;宿主 API 下限为 ≥ 0.1.1-rc.1)、Node ≥ 20、以及你要装到的 profile 名(下面示例用 web)。

dsh plugin --profile web add dsh-dupguard      # 把 web 换成你的 profile 名

重启 DSH 即生效(宿主代码只在进程启动时加载)。看到这两处就说明装好了:

  1. 设置页多出「重复守卫」分节,底部显示 设置通道:ready|构建 1.12.2|自动派生:检测窗口 …;
  2. 宿主日志出现 [dupguard] 生效参数:…(判断设置是否真的生效,只认这一行)。

默认值开箱可用:连续重复 ≥ 10 次即截停(已生成内容照常提交为助手消息),思考文本一并检测,代码区域 (围栏代码块、行内代码、缩进代码块三类统一)内按 4 倍放宽。想调整就在设置页改——改完无需重启 (写回 cordis.patch.yml,宿主自行热读取)。

桌面版(Electron):请在应用内「设置 → 插件」页面安装(desktop profile 由应用独占管理,CLI 会被拒绝)。
卸载 / 升级:dsh plugin --profile web remove|update dsh-dupguard;其它安装方式(本地仓库 --patch 挂载 /
动态插件 / 手工补丁层 / 临时禁用)见 备选安装方式。

兼容性

项目 · 支持范围 宿主 API · ≥ 0.1.1-rc.1(package.json 的 dsh.compatibility 声明值);一条命令 bundle 安装需 ≥ 0.1.2-rc.1 已实测通过(仓库内有记录) · DSH 0.2.1-rc.2(CLI / web profile 与桌面版,2026-10-20 复核)、DSH 0.2.1-rc.1(CLI / web profile,2026-10-17 复核)、DSH 0.2.0-rc.2(CLI / web profile 与桌面版)(见 兼容性报告);0.1.7-rc.2、0.1.7-rc.1、0.1.2-rc.1、0.1.1-rc.1(见 CHANGELOG 对应条目) 早期版本(未在本仓库留档) · 0.1.5-rc.2、0.1.6-alpha.2 曾在使用中验证过,但仓库内没有当时的记录,故不计入上方清单 Node · ≥ 20(与 DSH 一致,不支持 Node 18);.mjs 高级模块依赖运行时的 require(ESM) 支持(Node ≥ 20.19 / ≥ 22.12 已内置,更早版本回退并告警) 设置命名空间 · 0.1.7 起 = loader entry id(本 bundle 为 include:dupguard);≤ 0.1.6 为 dsh-dupguard。1.6.0 起运行时自动选路,无需按 DSH 版本换插件版本 升级提示 · 0.1.7 起设置键从 `dsh-dupguard:` 变为 `dupguard:`,升级后请在设置页重新保存一次(或把旧键内容手工挪到新键下)

  • 结论:升级 0.2.x(含 0.2.1-rc.2 / rc.1)无需改配置或换版本;桌面版与 CLI 同版本,上述 API 结论全部适用。
  • 旧版本配置直接沿用:配置键自 1.0.0 起只增不减、从未改名(1.0.0 七项 → 1.0.2 ignoredChars → 1.4.0

skipCodeBlocks/codeBlockMultiplier → 1.7.0 ignoredSubstrings → 1.8.0 thresholdMode/thresholdByLength/ advancedThresholdFile),取值范围只放宽过(codeBlockMultiplier 由 1–100 放宽到 0–100,其余自 1.3.0 起未变), 缺失的键一律取默认值;配置里出现当前 schema 未声明的键(如 fixStandingMountConflict、拼写错误)会被忽略 而不是让整节设置失效 ⇒ 当年通过设置页保存的配置,升级后行为不变(回归用例 S23 覆盖:键只增不减 + 取值范围只放宽)。

  • 客户端设置分节的静态依赖只有 slots / locale,设置服务用 ctx.inject() 动态接入 ⇒ **在已适配的三种设置

模型下**(settings.register、settingsScope、typert remote.settings)不会让入口卡在 pending;DSH 若引入 第四种模型仍需适配。

  • 每次核对的证据清单与历史记录(含可直接复制到 DSH Discussions 的报告):

docs/compatibility-report.md。

特性

  • 逐增量检测:每个 text-delta 到达即判定(增量可能含多个 token),命中即触发停止。
  • 停止方式:提前结束流 → 上游 iterator.return() → 适配器 finally 中 consumer.abort() 中止底层 HTTP 请求

(远端何时停止由服务端行为决定);不 `abort()` agent 步骤信号,并补发协议合规的 block-end + finish(stop),已生成内容正常提交为助手消息,不污染会话日志。

  • 多种复读形态:单字符、词语、带空格 / 换行分隔、逐行、前缀后循环(默认去空白后检测)。
  • 思考守卫:默认同时检测 reasoning(思考)文本,思考中的复读同样截停(monitorReasoning: false 可关)。
  • 图形化设置页(npm 常驻版):与「通用设置 / 模型 / 插件 / Agent 预设」并列的「重复守卫」分节,可视化编辑

白名单与全部检测参数,改完即时生效并持久化。按用户任务分四组(自上而下=从最常改到最少改):

分组 · 回答的问题 · 内容 重复次数(何时截停) · 次数怎么算 · 模式(简单 / 分段表 / 高级)+ 该模式的次数来源(阈值 / 三列表格 / 模块路径)+ 代码区域内阈值倍数 检测范围与开关 · 检测什么、按多长算 · 最小 / 最大重复单元长度 + 忽略空白、检测思考、检测工具参数三个开关 忽略白名单 · 哪些内容不算重复 · 字符与片段共用一个输入框(默认已覆盖 Markdown 表格分隔符) 截停通知 · 截停之后做什么 · 通知开关 + 系统通知开关(可选)+ 继续指令内容(关闭通知时后两项隐藏)

  • 截停通知 + 一键继续(npm 常驻版):每次截停都会弹出全局通知——与用户当前打开哪个会话无关,

通知上显示所属工作区、会话名称、重复的字符串、重复次数与命中通道;由用户选择是否「发送继续指令」 (注入到被截停的会话)或「不发送」。详见截停通知与继续指令。

  • 可选系统通知(npm 常驻版,默认关闭):打开后,页面不在前台时(标签页在后台、窗口最小化,或窗口失焦)被截停会额外发一条浏览器系统通知

(Windows 右下角 / macOS 通知中心),正文只有固定文案、不含工作区与会话名;首次开启时由浏览器请求授权。

  • 双入口交付:动态插件 plugin/host.js + npm 常驻版 lib/index.js /

lib/client.js;检测算法单源维护在 lib/engine.js,动态入口由 npm run build:host 生成,同一套用例驱动两个入口(CI 同时防「生成过期」与「行为漂移」)。

  • 截停记录(持久化日志,仅常驻版):每次截停追加一行 JSON 到 ~/.dsh/dupgard/stops.jsonl

(自动轮转;DSH_DUPGUARD_STOP_LOG=off 可关闭),详见「截停记录」一节。

  • 阈值决策窗口:命中阈值先暂停并弹出决策卡片,可「立即截停」或「允许继续」;不操作则超时后恢复观察,

达到「阈值 × 倍数」再自动截停(见「截停前的决策窗口」;无通知通道/动态版回退为阈值即停)。

  • 内置 DSH 兼容补丁(fixStandingMountConflict,默认开启;见「已知限制」)。

会拦住什么 / 不会拦什么

会拦住 · 不会拦住 单字符 / 词语 / 逐行复读达到阈值 · 正常文本里的高频词(只认连续重复) 带空格或换行分隔的复读 · 重复次数不足(默认 9 次及以下) 前缀之后的循环 · 工具调用参数(monitorToolArguments 默认关) 思考中的复读(默认开启) · Markdown 表格分隔行与长分隔线(-、\ · 默认在白名单里) 代码区域(围栏 / 行内 / 缩进)内的失控复读(在「阈值 × 倍数」处兜底) · 区域内的正常代码 / 测试夹具 / ASCII 图(默认倍数 4 放宽)

配置

默认值定义在两个入口顶部的 CONFIG(CI 校验行为一致)。npm 常驻版:除 fixStandingMountConflict 外全部可在 设置页「重复守卫」分节调整并持久化到 /cordis.patch.yml,改动即时生效;动态版固定取常量。

配置项 / Option · 默认 / Default · 说明 / Description threshold · 10 · 触发阈值,>= 语义(第 10 次重复出现即停)。范围 2–1000。仅简单模式在设置页显示;table/module 下只作兜底与回退值 thresholdMode · 'simple' · simple 固定阈值 / table 分段表 / module 高级模式(实验性,选中时弹出安全确认)(设置页下拉)。simple 不建表、走原有快路径 thresholdByLength · '' · table 模式的分段表:设置页为三列表格(起始只读 / 终止 / 次数,1–16 行;起始首行 1、其后 = 上一行终止 + 1),存储仍为 ":[, …]",例 1:40, 2:30, 8:12。末行终止 = 最大检测长度;旧写法 *: 仍兼容(迁移为末行) advancedThresholdFile · '' · 实验性:module 模式的 JavaScript 文件路径,导出 repeatCount(length) -> count(同步)。该文件在宿主进程内执行,选中高级模式时会弹出安全确认;详见「高级用法」 minUnitLength · 1 · 最小重复单元长度,范围 1–4096。表格模式下为派生值:等于表格起始(固定 1),由插件写回文档、不可手填 maxUnitLength · 80 · 最大重复单元长度,范围 1–8192。表格模式下为派生值:等于末行「终止」,由插件写回文档、不可手填 detectionWindow · 自动 · 派生值(只读):由插件计算并写回文档,手填值被忽略;公式见「检测窗口(自动推导)」 skipCodeBlocks · true · 代码区域分档总开关。⚠ 设置页已隐藏:`false` ≡ `codeBlockMultiplier: 1`,请改用倍数表达;老配置里的该键仍按原语义生效,改动倍数时会自动清除它 codeBlockMultiplier · 4 · 代码区域(围栏 / 行内 / 缩进)内统一的阈值倍数,设置页标签为「代码内阈值倍数」。三档:≥2 按「阈值 × 倍数」判定(越大越不易误杀,也越晚兜住失控);1 与区域外同样严格;0 完全不检测区域内(且区域两侧不拼接)。范围 0–100 stripWhitespace · true · 检测前移除空白 / 换行,识别 x x x、x\nx\nx 这类带分隔符的复读 ignoredChars · ['-', '\ · '] · 逐字符白名单(Markdown 表格分隔行不计入)。条目必须是单个字符(按 Unicode 码点);多字符 / 空条目运行时丢弃并告警。设置页与下一个字段共用一个输入框 ignoredSubstrings · [] · 片段白名单(多字符):整段字面量匹配(区分大小写、不支持正则),命中时先整段剔除,再走去空白与逐字符规则,长片段优先。每项 ≤ 64 码点、最多 64 项。代价:跨增量匹配需每块保留(最长片段 − 1)个字符不参与检测。设置页与上一个字段共用一个输入框:填 1 个码点且非空白 ⇒ 自动进 ignoredChars,其余(多字符片段、单字符空白)⇒ 自动进 ignoredSubstrings;多个条目用空格 / 逗号分隔(- \ · 加两条,-\ · 是一个片段) monitorReasoning · true · 同时检测思考(reasoning)文本;只检测可见输出时置 false monitorToolArguments · false · 同时检测工具调用参数(JSON / base64 里重复字符常见,默认关) fixStandingMountConflict · true · standing-mount 冲突补丁(默认开启;在 `0.1.1-rc.1` / `0.1.2-rc.1` 上实测该缺陷存在,更高版本未逐版本实测;仅代码常量,见「已知限制」) notifyOnStop · true · 截停时是否弹出全局通知(见下一节)。关掉只是不再弹通知,截停行为不变 systemNotify · false · 截停时是否同时发一条系统通知(浏览器 Notification → Windows 右下角 / macOS 通知中心)。只在 DSH 页面不在前台时弹(标签页在后台、窗口最小化,或窗口失焦);正文是固定文案(不含工作区路径、会话名与重复内容);首次打开开关时请求浏览器授权。需要 DSH 网页处于打开状态(后台标签页即可) persistStopLog · true · 是否把每次截停追加写入持久化日志(~/.dsh/dupgard/stops.jsonl,超过 2MB 自动轮转;仅 npm/常驻版可用)。默认脱敏(不写重复内容/会话标题/工作区路径);DSH_DUPGUARD_STOP_LOG_REDACT=0 才记录完整内容。环境变量 DSH_DUPGUARD_STOP_LOG=off 是更高优先级的总开关。详见「截停记录」 stopDecision · true · 阈值决策窗口总开关。开启且通知通道可用时,命中阈值先暂停并通知,由用户选择立即截停/允许继续;不操作则超时恢复并观察。关闭 → 沿用「阈值即停」 decisionTimeoutMs · 4000 · 阈值处暂停等待用户操作的时长(毫秒,1000–15000,步长 500)。超时后恢复生成并进入观察期 stopAtMultiplier · 2 · 观察期兜底倍数(2–10):同一单元实际重复达到「阈值 × 倍数」时自动截停;重复中途中断则取消本次决策 continuePrompt · '请从中断处继续,不要重复之前的内容。' · 用户在通知里点「发送继续指令」时注入到该会话的用户消息文本(≤500 码点;留空回落默认)

高级用法

按长度定次数的语义:检测对每个候选周期 p 分别取 need(p) 判定,而一段周期性文本在其周期的整数倍上 同样是合法重复。因此只抬高长度 1 的次数通常无效(同一段 aaaa… 会以 p=2、3… 命中),想真正放宽短串必须把 所有可能命中的小周期一起抬高,例如 1:40, 2:40, 3:40, *:10(39 个同字符不触发、40 个触发);反过来,长单元只需 更少次数即可单独生效(模块返回「长度 ≥ 20 → 3 次」时,20 字符单元重复 3 次即截停)。

优先级与回退链:module 加载成功 ⇒ 用模块;module 失败 ⇒ 基础 `threshold`(告警一次,不静默回落到分段表); table 逐项查表 ⇒ 未覆盖用 * ⇒ 否则 threshold。

分段表模式:候选区间完全由表格决定——最小单元长度固定为 1、最大单元长度 = 末行终止,两者都是派生值 (写回文档、只在设置页底部诊断行显示);界面上唯一可编辑的数值是「代码块内阈值倍数」。需要更多台阶(> 16 行)、 非单调规则或按公式计算(如 max(3, ceil(200/p)))时才必须改用模块。

模块模式(实验性):入参是清洗后的单元长度(Unicode 码点),返回所需连续重复次数 (整数 2–1000;< 2 或非整数 ⇒ 该长度不判定);必须同步返回。宿主启动与设置变更时会打印「次数策略」摘要 (模式、覆盖长度数、次数区间、最长跨度),据此确认是否真的生效。

设置为实验性功能:下拉里显示为「高级模式(实验性)」。从其它模式切到高级模式时会弹出风险确认
(与 DSH 的「完全权限」弹窗同一模式):必须勾选「我已了解风险,并愿意继续」才能点「启用高级模式」;
取消则保持原模式、不写入任何设置。已处于高级模式时重复选择不再弹窗,离开后再次进入会重新弹窗。
⚠ 安全提示:模块文件在 DSH 宿主进程内执行,拥有与 DSH 相同的权限——可读写文件、发起网络请求或执行
任意代码;插件无法校验文件内容,请只指向自己完全信任的来源。扩展名不限:.js / .cjs 最稳妥;
.mjs 依赖运行时的 require(ESM) 支持(Node ≥ 20.19 / ≥ 22.12 已内置,更早版本回退并告警);
其它扩展名(如 `.txt`)Node 也按 CommonJS 加载;.json 仅作数据、按 JSON 解析因而无法导出函数。
保持同步、廉价、无副作用:插件只夹取返回值,加载或调用失败时整体回退基础阈值。

功能等价关系(知道后就不必纠结用哪一个):

等价关系 · 说明 skipCodeBlocks: false ≡ codeBlockMultiplier: 1 · 两者都表示「块内与块外同样严格」。开关已隐藏,统一用倍数表达(0 = 不检测块内 / 1 = 关闭分档 / ≥2 = 放宽到「阈值 × 倍数」),且改倍数会自动清除旧键 单行表格 N:C ≡ 简单模式 threshold: C + maxUnitLength: N · 单行表格的候选区间就是 1..N、次数恒为 C,窗口派生值也相同 常量模块 () => C ≡ 简单模式(阈值 C) · 模块只是把次数表达成函数 阶梯模块 ≡ 分段表 · 表格最多 16 行且必须连续覆盖;需要更多台阶、非单调规则或按公式计算时才必须用模块 单字符 ignoredSubstrings 条目 ≡ ignoredChars 条目 · 对单个非空白字符两者检出结果相同(剔除以字符为单位,先剔片段还是先去空白不影响结果),因此设置页只给一个输入框:1 码点且非空白自动进字符表,其余自动进片段表;字符表是默认开启的便捷项,片段表额外支持多字符片段 thresholdByLength 里的 *: · 仅兼容手改配置:设置页只产生连续覆盖的表格(不会写 *) detectionWindow 手填值 · 会被派生值覆盖(宿主不读它),设置页已不再提供输入

该用哪个:一个阈值就够 → 简单模式;几条阶梯又不想写代码 → 分段表;按公式 / 非单调 / 长平台期 → 高级模式(实验性,需通过安全确认)。

先用隔离环境验一遍(强烈建议):高级模式的模块是在宿主进程内、以宿主权限执行的,所以在填进真实 profile 之前 先用仓库自带的沙箱试运行——它在受限子进程里加载模块、复用插件的真实判次代码,并打印每个长度对应的生效次数:

npm run sandbox:module -- <你的模块文件>        # 写文件/子进程被拒、死循环被超时掐断、网络不受限(见下)
npm run sandbox:selftest                        # 六个内置样例的预期结论核对(CI 也跑)

完整的"三层隔离"做法(沙箱试运行 → 独立 profile + 一次性工作区 → 独立 DSH_HOME / WSL)见 docs/advanced-mode-sandbox.md;其中独立 profile(`advtest`)与一次性工作区 `D:\ZQH260619\repos\dupguard-advtest` 已预置好,用 powershell -File tools\run-advtest.ps1 启动 (脚本会清掉从当前 DSH 会话继承的 DSH_PROFILE_DIR 等变量——否则插件会读到别的 profile 的设置,实测会把模式覆写成 table)。 实测生效参数:模式=module|覆盖长度 1000 种|次数区间 3–20|最长跨度 3000 字符(正是样例模块的返回值 ⇒ 模块已被真实宿主加载并驱动策略)。

代码区域判定(围栏 / 行内 / 缩进,三类统一):三类区域都用同一个 阈值 threshold × codeBlockMultiplier,优先级为 围栏 > 缩进 > 行内:

代码块里出现"非故障的长重复"(框线、分隔线、ASCII 图)怎么办? 用 codeBlockMultiplier,不要往白名单里塞字符——
白名单是全局的(会让"模型刷符号空转"这类真实故障也漏检),而倍数只作用于代码区域。实测(npm run audit:false-positives
把真实仓库的源码包进围栏重放检测器):默认配置在 260 个文件里误杀 2 个(─ 框线、一段 a);把任意符号字符
(= * / . # _ ~ + 0 1)加进白名单一处都没减少;而把代码内倍数从 3 提到 4(即现在的默认值;代码区内阈值 30→40)就降到了 0。

区域 · 进入 · 退出 / 边界 围栏代码块 · 行首 ≤3 空格 + 连续 ≥3 个反引号或 ~(其后为 info string) · 同字符、不短于起始长度的 run 行(其后仅空白);未闭合则延续到块结束 缩进代码块 · 行首缩进 ≥4(制表符按 4 计,\r 不计入)且上一行为空行,且上一个非空行不是列表项 / 引用起始 · 遇到首个「非空且缩进 <4」的行;块内空行不终止。列表项之后的缩进与段落续行不算代码(保守规则,避免误判) 行内代码 · 1–64 个反引号开启(1 个、2 个……最多 64 个都可以) · 必须在同一行内由等长反引号 run 闭合;换行 / 保留超过 256 字符 / 块结束仍未闭合 ⇒ 开启符按普通文本,内容照常参与检测

  • 倍数为 0 时三类区域内部都不再判定,且区域边界会清空检测缓冲(区域两侧的文本不会被拼成人为重复)——

这是「代码再长也不误杀」的代价,默认 4 会在「阈值 × 4」处兜底;

  • 围栏内的反引号/缩进不会另开区域;4 空格缩进的 ``` ``` 也不算围栏(仍留在缩进代码块内);
  • 行尾兼容 CRLF:\r 视为空白(不计入缩进),因此 \r\n 文档里的围栏结束行、空行与缩进代码块都按行正确判定;
  • 行内区需要有界保留(开启符之后最多 256 字符不发射),因此超过 256 字符的单行行内代码会被按普通文本判定

(已知限制,见下文);未闭合反引号会在宿主日志告警一次;

  • 老配置若显式设过 skipCodeBlocks: false,设置页会在倍数行下方提示一次(等价于倍数 1)。

检测窗口(自动推导)