dsh-dupguard 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +9 -6
- package/lib/index.js +30 -6
- package/package.json +1 -1
- package/plugin/host.js +30 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
本文件遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [SemVer](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [1.0.2] - 2026-08-27
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- 修复 Markdown 表格误截停:新增 `ignoredChars` 白名单(默认 `['-', '|']`),
|
|
10
|
+
表格分隔行(如 `|---|---|`)与长分隔线不再被误判为复读;夹带白名单字符的真实复读
|
|
11
|
+
(如 `-ab-` ×10)仍会被识别。测试套件新增 4 项,双入口各 20 项。
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- 验证与 DSH `0.1.1-rc.1` 兼容:`llm/stream` 事件签名、`StreamChunk` 协议、适配器关闭语义
|
|
16
|
+
(`consumer.abort`)、`llm-invariant` 校验、`BlockAssembler`、agent-loop 流消费、
|
|
17
|
+
`CordisInspectRegistryService`(`providers` 字段与 `register` 语义)均无变化,插件无需改动;
|
|
18
|
+
`dsh-agent-presets` 的 standing-mount 多代并存缺陷 `0.1.1-rc.1` 仍未修复,兼容补丁
|
|
19
|
+
(`fixStandingMountConflict`)仍然必要(文档表述更新为"≤ 0.1.1-rc.1")。
|
|
20
|
+
- 验证与 DSH `0.1.0-rc.7` 兼容:`llm/stream` 事件签名、`StreamChunk` 协议、适配器关闭语义
|
|
21
|
+
(`consumer.abort`)、`llm-invariant` 校验、`BlockAssembler`、agent-loop 流消费、
|
|
22
|
+
`CordisInspectRegistryService`(`providers` 字段与 `register` 语义)均与 rc.6 一致,
|
|
23
|
+
插件无需改动;`dsh-agent-presets` 的 standing-mount 多代并存缺陷 rc.7 仍未修复,
|
|
24
|
+
兼容补丁(`fixStandingMountConflict`)仍然必要(文档表述由"≤ rc.6"更新为"≤ rc.7")。
|
|
25
|
+
|
|
5
26
|
## [1.0.1] - 2026-08-17
|
|
6
27
|
|
|
7
28
|
### Added
|
package/README.md
CHANGED
|
@@ -21,10 +21,11 @@ When triggered, the already-generated text is committed as a normal assistant me
|
|
|
21
21
|
- **多种复读形态**:单字符循环、词语循环、带空格/换行分隔的复读均能识别(默认去空白后检测)。
|
|
22
22
|
- **真正的服务端停止**:提前关闭流迭代 → 适配器 `consumer.abort()` → 中断 HTTP 连接,模型在服务端停止生成。
|
|
23
23
|
- **安全停止**:绝不 `abort()` agent 步骤信号;补发协议合规的 `block-end` + `finish(stop)`,消息正常提交。
|
|
24
|
+
- **Markdown 表格友好**:默认忽略连字符与竖线(`ignoredChars` 白名单,可配置),表格分隔行与长分隔线不会被误判为复读。
|
|
24
25
|
- **零依赖 / 零配置**:纯 JavaScript,无运行时依赖;默认配置开箱即用。
|
|
25
26
|
- **双入口交付**:动态插件(`plugin/host.js`)+ npm 组合挂载(`lib/index.js`),行为一致、CI 防漂移。
|
|
26
27
|
- **内置 DSH 兼容补丁**(`fixStandingMountConflict`,默认开启):幂等化 `cordisInspect.register`,
|
|
27
|
-
修复 DSH ≤ rc.
|
|
28
|
+
修复 DSH ≤ 0.1.1-rc.1 的 preset standing-mount 多代并存冲突(见下文"已知限制")。
|
|
28
29
|
|
|
29
30
|
---
|
|
30
31
|
|
|
@@ -104,9 +105,10 @@ Edit the `CONFIG` block at the top of `plugin/host.js` / `lib/index.js` (both en
|
|
|
104
105
|
| `maxUnitLength` | `80` | 最大重复单元长度 / maximum repeating-unit length |
|
|
105
106
|
| `detectionWindow` | `8192` | 检测滚动窗口(字符,去空白后)/ rolling detection window in chars (after whitespace removal) |
|
|
106
107
|
| `stripWhitespace` | `true` | 检测前移除空白/换行,识别带分隔符的复读 / strip whitespace so `"x x x"` and `"x\nx\nx"` are caught |
|
|
108
|
+
| `ignoredChars` | `['-', '\|']` | 检测时忽略的字符白名单:Markdown 表格分隔行(连字符与竖线)不参与重复统计;需更严格检测时置 `[]` / whitelist of characters ignored during detection: Markdown table separators don't count as repetition; set `[]` for stricter detection |
|
|
107
109
|
| `monitorReasoning` | `false` | 是否检测思考文本 / also guard reasoning (thinking) text — off by default, high false-positive risk |
|
|
108
110
|
| `monitorToolArguments` | `false` | 是否检测工具调用参数 / also guard tool-call JSON args — off by default (base64/JSON repeats are common) |
|
|
109
|
-
| `fixStandingMountConflict` | `true` | DSH ≤ rc.
|
|
111
|
+
| `fixStandingMountConflict` | `true` | DSH ≤ 0.1.1-rc.1 兼容补丁:幂等化 `cordisInspect.register`,修复 preset standing-mount 多代并存冲突 / idempotent `cordisInspect.register` patch for the DSH ≤ 0.1.1-rc.1 standing-mount conflict |
|
|
110
112
|
|
|
111
113
|
---
|
|
112
114
|
|
|
@@ -160,8 +162,9 @@ agent-loop commits the partial text as a normal assistant message.
|
|
|
160
162
|
| 前缀后循环 / loop after prefix | `好的,下面开始回答:` + `循环` ×10 |
|
|
161
163
|
|
|
162
164
|
**不会触发 / Won't trigger**:正常文本中的高频词(检测只针对**连续**重复)、重复 9 次及以下、
|
|
163
|
-
reasoning
|
|
164
|
-
≤9 repeats, reasoning and tool args
|
|
165
|
+
reasoning 与工具参数(默认关闭)、Markdown 表格分隔行与长分隔线(连字符与竖线在白名单中,默认忽略)。
|
|
166
|
+
/ high-frequency words in normal prose (consecutive runs only), ≤9 repeats, reasoning and tool args
|
|
167
|
+
(off by default), Markdown table separator rows and horizontal rules (whitelisted by default).
|
|
165
168
|
|
|
166
169
|
---
|
|
167
170
|
|
|
@@ -205,7 +208,7 @@ Node 18/20/22.
|
|
|
205
208
|
- 服务端停止依赖适配器在流关闭时中止底层请求的语义(已验证 `dsh-llm-deepseek`;自定义适配器需自查)。
|
|
206
209
|
- 阈值语义为 `>= threshold`:第 10 次重复出现时即停止。
|
|
207
210
|
|
|
208
|
-
### DSH 运行期间编辑 preset 后的 standing-mount 冲突(DSH ≤ rc.
|
|
211
|
+
### DSH 运行期间编辑 preset 后的 standing-mount 冲突(DSH ≤ 0.1.1-rc.1 缺陷,本插件已内置补丁)
|
|
209
212
|
|
|
210
213
|
**现象**:对某个会话执行模型选择等操作时报
|
|
211
214
|
`resume failed ... preset ... failed to mount ... Host Cordis inspect provider "Service" is already registered`,
|
|
@@ -221,7 +224,7 @@ while the process lives")。`tool-cordis` 在每次挂载时向**进程全局*
|
|
|
221
224
|
**本插件的修复(默认开启)**:`apply` 时把 `cordisInspect.register` **幂等化**——同 id 已有注册时
|
|
222
225
|
共享既有注册并返回 no-op disposer,多代并存不再冲突。补丁进程内常驻(卸载本插件后仍生效,
|
|
223
226
|
重启后由本插件重新安装;HMR 重载不会叠加)。依赖 `cordisInspect.providers` 为可读 Map
|
|
224
|
-
(rc.6 实测如此);DSH 升级修复后可将 `CONFIG.fixStandingMountConflict` 置为 `false` 关闭。
|
|
227
|
+
(rc.6 / rc.7 / 0.1.1-rc.1 实测如此);DSH 升级修复后可将 `CONFIG.fixStandingMountConflict` 置为 `false` 关闭。
|
|
225
228
|
|
|
226
229
|
**仍建议的操作纪律**:运行期间编辑已挂载 preset 后重启 DSH(补丁消除的是报错,旧代残留的
|
|
227
230
|
组合仍占用资源,这是 DSH 的既有行为);根治仍待上游修复。
|
package/lib/index.js
CHANGED
|
@@ -28,12 +28,17 @@ const CONFIG = {
|
|
|
28
28
|
// 检测前先移除所有空白字符(含换行):
|
|
29
29
|
// 让 "重复 重复 重复"、"重复\n重复\n重复" 这类带分隔符的复读也能被识别。
|
|
30
30
|
stripWhitespace: true,
|
|
31
|
+
// 检测时忽略的字符(白名单):Markdown 表格的分隔行由连字符与竖线组成
|
|
32
|
+
// (如 "|---|---|"),正常表格输出会大量连续出现,不应视为复读。
|
|
33
|
+
// 默认忽略连字符与竖线;需要更严格的检测时可改为空数组 []。
|
|
34
|
+
ignoredChars: ['-', '|'],
|
|
31
35
|
// 是否同时检测思考(reasoning)文本。默认关闭:思考内容不可见,且正常思考文本
|
|
32
36
|
// 更可能出现连续重复片段,误伤风险高。
|
|
33
37
|
monitorReasoning: false,
|
|
34
38
|
// 是否同时检测工具调用参数(JSON 片段)。默认关闭:JSON / base64 中重复字符很常见。
|
|
35
39
|
monitorToolArguments: false,
|
|
36
|
-
// DSH ≤ rc.
|
|
40
|
+
// DSH ≤ 0.1.1-rc.1 兼容补丁(默认开启;实测 0.1.1-rc.1 仍未修复):preset 的 standing mount
|
|
41
|
+
// 在 composition 文件变化后
|
|
37
42
|
// 会新建一代而旧代永不销毁,tool-cordis 每次挂载都向进程全局的 cordisInspect 注册表
|
|
38
43
|
// 注册 Service/Event/Builtin/Tool provider,两代并存即抛
|
|
39
44
|
// "Host Cordis inspect provider ... is already registered"(表现为:截停后模型操作报
|
|
@@ -76,6 +81,24 @@ function stripWhitespace(text) {
|
|
|
76
81
|
return text.replace(/\s+/g, '')
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
/** 移除白名单字符(与 CONFIG.ignoredChars 配合)。 */
|
|
85
|
+
function stripIgnoredChars(text, ignored) {
|
|
86
|
+
if (ignored.length === 0) return text
|
|
87
|
+
let out = ''
|
|
88
|
+
for (let i = 0; i < text.length; i++) {
|
|
89
|
+
const ch = text[i]
|
|
90
|
+
if (ignored.indexOf(ch) === -1) out += ch
|
|
91
|
+
}
|
|
92
|
+
return out
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 供检测使用的增量清洗:去空白 + 移除白名单字符。 */
|
|
96
|
+
function sanitizeDelta(delta, config) {
|
|
97
|
+
let piece = config.stripWhitespace ? stripWhitespace(delta) : delta
|
|
98
|
+
if (config.ignoredChars.length > 0) piece = stripIgnoredChars(piece, config.ignoredChars)
|
|
99
|
+
return piece
|
|
100
|
+
}
|
|
101
|
+
|
|
79
102
|
/**
|
|
80
103
|
* 为一次 llm/stream 调用创建守卫。
|
|
81
104
|
* 每次模型调用都会新建一份状态,互不干扰。
|
|
@@ -107,7 +130,7 @@ function createStreamGuard(options) {
|
|
|
107
130
|
/** 把一段文本增量喂给检测缓冲,返回命中结果(null 表示未命中)。 */
|
|
108
131
|
function feedText(b, delta) {
|
|
109
132
|
b.text += delta
|
|
110
|
-
const piece =
|
|
133
|
+
const piece = sanitizeDelta(delta, CONFIG)
|
|
111
134
|
b.stripped = (b.stripped + piece).slice(-CONFIG.detectionWindow)
|
|
112
135
|
return findRepeatedTail(b.stripped, CONFIG.threshold, CONFIG.minUnitLength, CONFIG.maxUnitLength)
|
|
113
136
|
}
|
|
@@ -140,7 +163,8 @@ function createStreamGuard(options) {
|
|
|
140
163
|
if (chunk.name !== undefined) b.toolCallName = chunk.name
|
|
141
164
|
b.toolCallArguments += chunk.argumentsDelta
|
|
142
165
|
if (CONFIG.monitorToolArguments) {
|
|
143
|
-
|
|
166
|
+
const piece = sanitizeDelta(chunk.argumentsDelta, CONFIG)
|
|
167
|
+
b.stripped = (b.stripped + piece).slice(-CONFIG.detectionWindow)
|
|
144
168
|
const hit = findRepeatedTail(b.stripped, CONFIG.threshold, CONFIG.minUnitLength, CONFIG.maxUnitLength)
|
|
145
169
|
if (hit !== null) stopped = hit
|
|
146
170
|
}
|
|
@@ -214,7 +238,7 @@ function createStreamGuard(options) {
|
|
|
214
238
|
}
|
|
215
239
|
|
|
216
240
|
/**
|
|
217
|
-
* DSH ≤ rc.
|
|
241
|
+
* DSH ≤ 0.1.1-rc.1 standing-mount 兼容补丁:把 cordisInspect.register 幂等化。
|
|
218
242
|
*
|
|
219
243
|
* 背景:preset 的 standing mount 在 composition 文件变化后新建一代、旧代永不销毁;
|
|
220
244
|
* tool-cordis 每次挂载都向进程全局的 cordisInspect 注册 Service/Event/Builtin/Tool
|
|
@@ -222,8 +246,8 @@ function createStreamGuard(options) {
|
|
|
222
246
|
* 注册(返回 no-op disposer),多代并存不再冲突。
|
|
223
247
|
*
|
|
224
248
|
* 说明:补丁有意不做撤销(常驻进程,重启后由本插件重新安装);依赖
|
|
225
|
-
* cordisInspect.providers 为可读 Map(实测 rc.6 如此)。
|
|
226
|
-
* CONFIG.fixStandingMountConflict 置为 false。
|
|
249
|
+
* cordisInspect.providers 为可读 Map(实测 rc.6 / rc.7 / 0.1.1-rc.1 如此)。
|
|
250
|
+
* DSH 升级修复后可将 CONFIG.fixStandingMountConflict 置为 false。
|
|
227
251
|
*/
|
|
228
252
|
function installStandingMountPatch(ctx) {
|
|
229
253
|
const inspect = ctx.get('cordisInspect')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-dupguard",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Real-time repetition guard for DeepSeek Harness (DSH): stops model generation when the same string repeats >=10 times in the streamed output. 实时检测 DSH 大模型流式输出中的重复内容,同一字符串重复十次以上立即停止生成。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|
package/plugin/host.js
CHANGED
|
@@ -25,12 +25,17 @@ const CONFIG = {
|
|
|
25
25
|
// 检测前先移除所有空白字符(含换行):
|
|
26
26
|
// 让 "重复 重复 重复"、"重复\n重复\n重复" 这类带分隔符的复读也能被识别。
|
|
27
27
|
stripWhitespace: true,
|
|
28
|
+
// 检测时忽略的字符(白名单):Markdown 表格的分隔行由连字符与竖线组成
|
|
29
|
+
// (如 "|---|---|"),正常表格输出会大量连续出现,不应视为复读。
|
|
30
|
+
// 默认忽略连字符与竖线;需要更严格的检测时可改为空数组 []。
|
|
31
|
+
ignoredChars: ['-', '|'],
|
|
28
32
|
// 是否同时检测思考(reasoning)文本。默认关闭:思考内容不可见,且正常思考文本
|
|
29
33
|
// 更可能出现连续重复片段,误伤风险高。
|
|
30
34
|
monitorReasoning: false,
|
|
31
35
|
// 是否同时检测工具调用参数(JSON 片段)。默认关闭:JSON / base64 中重复字符很常见。
|
|
32
36
|
monitorToolArguments: false,
|
|
33
|
-
// DSH ≤ rc.
|
|
37
|
+
// DSH ≤ 0.1.1-rc.1 兼容补丁(默认开启;实测 0.1.1-rc.1 仍未修复):preset 的 standing mount
|
|
38
|
+
// 在 composition 文件变化后
|
|
34
39
|
// 会新建一代而旧代永不销毁,tool-cordis 每次挂载都向进程全局的 cordisInspect 注册表
|
|
35
40
|
// 注册 Service/Event/Builtin/Tool provider,两代并存即抛
|
|
36
41
|
// "Host Cordis inspect provider ... is already registered"(表现为:截停后模型操作报
|
|
@@ -73,6 +78,24 @@ function stripWhitespace(text) {
|
|
|
73
78
|
return text.replace(/\s+/g, '')
|
|
74
79
|
}
|
|
75
80
|
|
|
81
|
+
/** 移除白名单字符(与 CONFIG.ignoredChars 配合)。 */
|
|
82
|
+
function stripIgnoredChars(text, ignored) {
|
|
83
|
+
if (ignored.length === 0) return text
|
|
84
|
+
let out = ''
|
|
85
|
+
for (let i = 0; i < text.length; i++) {
|
|
86
|
+
const ch = text[i]
|
|
87
|
+
if (ignored.indexOf(ch) === -1) out += ch
|
|
88
|
+
}
|
|
89
|
+
return out
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 供检测使用的增量清洗:去空白 + 移除白名单字符。 */
|
|
93
|
+
function sanitizeDelta(delta, config) {
|
|
94
|
+
let piece = config.stripWhitespace ? stripWhitespace(delta) : delta
|
|
95
|
+
if (config.ignoredChars.length > 0) piece = stripIgnoredChars(piece, config.ignoredChars)
|
|
96
|
+
return piece
|
|
97
|
+
}
|
|
98
|
+
|
|
76
99
|
/**
|
|
77
100
|
* 为一次 llm/stream 调用创建守卫。
|
|
78
101
|
* 每次模型调用都会新建一份状态,互不干扰。
|
|
@@ -104,7 +127,7 @@ function createStreamGuard(options) {
|
|
|
104
127
|
/** 把一段文本增量喂给检测缓冲,返回命中结果(null 表示未命中)。 */
|
|
105
128
|
function feedText(b, delta) {
|
|
106
129
|
b.text += delta
|
|
107
|
-
const piece =
|
|
130
|
+
const piece = sanitizeDelta(delta, CONFIG)
|
|
108
131
|
b.stripped = (b.stripped + piece).slice(-CONFIG.detectionWindow)
|
|
109
132
|
return findRepeatedTail(b.stripped, CONFIG.threshold, CONFIG.minUnitLength, CONFIG.maxUnitLength)
|
|
110
133
|
}
|
|
@@ -137,7 +160,8 @@ function createStreamGuard(options) {
|
|
|
137
160
|
if (chunk.name !== undefined) b.toolCallName = chunk.name
|
|
138
161
|
b.toolCallArguments += chunk.argumentsDelta
|
|
139
162
|
if (CONFIG.monitorToolArguments) {
|
|
140
|
-
|
|
163
|
+
const piece = sanitizeDelta(chunk.argumentsDelta, CONFIG)
|
|
164
|
+
b.stripped = (b.stripped + piece).slice(-CONFIG.detectionWindow)
|
|
141
165
|
const hit = findRepeatedTail(b.stripped, CONFIG.threshold, CONFIG.minUnitLength, CONFIG.maxUnitLength)
|
|
142
166
|
if (hit !== null) stopped = hit
|
|
143
167
|
}
|
|
@@ -211,7 +235,7 @@ function createStreamGuard(options) {
|
|
|
211
235
|
}
|
|
212
236
|
|
|
213
237
|
/**
|
|
214
|
-
* DSH ≤ rc.
|
|
238
|
+
* DSH ≤ 0.1.1-rc.1 standing-mount 兼容补丁:把 cordisInspect.register 幂等化。
|
|
215
239
|
*
|
|
216
240
|
* 背景:preset 的 standing mount 在 composition 文件变化后新建一代、旧代永不销毁;
|
|
217
241
|
* tool-cordis 每次挂载都向进程全局的 cordisInspect 注册 Service/Event/Builtin/Tool
|
|
@@ -219,8 +243,8 @@ function createStreamGuard(options) {
|
|
|
219
243
|
* 注册(返回 no-op disposer),多代并存不再冲突。
|
|
220
244
|
*
|
|
221
245
|
* 说明:补丁有意不做撤销(常驻进程,重启后由本插件重新安装);依赖
|
|
222
|
-
* cordisInspect.providers 为可读 Map(实测 rc.6 如此)。
|
|
223
|
-
* CONFIG.fixStandingMountConflict 置为 false。
|
|
246
|
+
* cordisInspect.providers 为可读 Map(实测 rc.6 / rc.7 / 0.1.1-rc.1 如此)。
|
|
247
|
+
* DSH 升级修复后可将 CONFIG.fixStandingMountConflict 置为 false。
|
|
224
248
|
*/
|
|
225
249
|
function installStandingMountPatch(ctx) {
|
|
226
250
|
const inspect = ctx.get('cordisInspect')
|