dsh-dupguard 1.0.0 → 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 +30 -0
- package/README.md +83 -8
- package/lib/index.js +66 -2
- package/package.json +1 -1
- package/plugin/host.js +66 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
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
|
+
|
|
26
|
+
## [1.0.1] - 2026-08-17
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- DSH ≤ rc.6 兼容补丁(`fixStandingMountConflict`,默认开启):幂等化 `cordisInspect.register`,
|
|
31
|
+
修复 preset standing-mount 多代并存导致的 "Host Cordis inspect provider ... is already registered"
|
|
32
|
+
(截停后模型操作报 resume failed、必须重启 DSH 才能恢复)问题。
|
|
33
|
+
- 测试套件新增第 16 项(幂等补丁行为),双入口各 16 项。
|
|
34
|
+
|
|
5
35
|
## [1.0.0] - 2026-08-16
|
|
6
36
|
|
|
7
37
|
### Added
|
package/README.md
CHANGED
|
@@ -21,8 +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 防漂移。
|
|
27
|
+
- **内置 DSH 兼容补丁**(`fixStandingMountConflict`,默认开启):幂等化 `cordisInspect.register`,
|
|
28
|
+
修复 DSH ≤ 0.1.1-rc.1 的 preset standing-mount 多代并存冲突(见下文"已知限制")。
|
|
26
29
|
|
|
27
30
|
---
|
|
28
31
|
|
|
@@ -39,16 +42,53 @@ When triggered, the already-generated text is committed as a normal assistant me
|
|
|
39
42
|
|
|
40
43
|
Paste the entire content of [`plugin/host.js`](plugin/host.js) as `code.host` in `cordis_define`, then activate the returned `packageId` with `cordis_run`.
|
|
41
44
|
|
|
42
|
-
### 方式二:npm 安装 +
|
|
45
|
+
### 方式二:npm 安装 + 组合挂载(常驻,随 DSH 启动)/ npm + composition (persistent)
|
|
46
|
+
|
|
47
|
+
插件已发布到 npm:[`dsh-dupguard`](https://www.npmjs.com/package/dsh-dupguard)。
|
|
48
|
+
|
|
49
|
+
**1. 在 DSH profile 目录安装依赖**(例如 web GUI 的 `$DSH_HOME/profiles/web`):
|
|
43
50
|
|
|
44
51
|
```bash
|
|
45
|
-
npm install dsh-dupguard
|
|
52
|
+
pnpm add dsh-dupguard # 或 npm install dsh-dupguard
|
|
46
53
|
```
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
[deepseek-ai/deepseek-harness · docs/user/develop/basic/publish.zh.md](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.zh.md)(npm 生态与发布流程)。
|
|
55
|
+
**2. 在 profile 的用户补丁层 `cordis.patch.yml` 插入组合行**:
|
|
50
56
|
|
|
51
|
-
|
|
57
|
+
```yaml
|
|
58
|
+
# $DSH_HOME/profiles/web/cordis.patch.yml
|
|
59
|
+
- insert:
|
|
60
|
+
- id: dupguard
|
|
61
|
+
name: dsh-dupguard
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- 用户补丁层在 bundle 层之后、`--patch` 之前应用;运行中的 DSH 通过 `watchUserPatches`
|
|
65
|
+
**热重载**它——保存即生效,无需重启;加载失败会事务性回滚,不影响应用。
|
|
66
|
+
- 验证:`dsh --profile web --dump-config` 末尾应出现 `dupguard` 行;loader 日志会出现
|
|
67
|
+
`apply plugin dupguard`。
|
|
68
|
+
- **撤销**:删掉该 `insert` 项即可热卸载。
|
|
69
|
+
|
|
70
|
+
**本机当前状态**:`profiles/web` 的组合行仍以 `file:` URL 直连仓库 `lib/index.js`
|
|
71
|
+
(发布前的过渡形态)。切换到 npm 包只需两步:在 `$DSH_HOME/profiles/web` 下执行
|
|
72
|
+
`pnpm add dsh-dupguard`,再把 `cordis.patch.yml` 中该行的 `name` 改为 `dsh-dupguard`。
|
|
73
|
+
|
|
74
|
+
**本地开发替代**:未发布/调试时,`name` 也可直接用 `file:` URL 指向仓库内的
|
|
75
|
+
[`lib/index.js`](lib/index.js)(CJS 导出 `{ name, apply }`,与 loader 的
|
|
76
|
+
`unwrapExports` 兼容,零构建)。
|
|
77
|
+
|
|
78
|
+
The plugin is published on npm as [`dsh-dupguard`](https://www.npmjs.com/package/dsh-dupguard).
|
|
79
|
+
Install it inside the DSH profile (e.g. `pnpm add dsh-dupguard` under
|
|
80
|
+
`$DSH_HOME/profiles/web`), then insert the row `{ id: dupguard, name: dsh-dupguard }` into the
|
|
81
|
+
profile's user patch layer `cordis.patch.yml`. The running DSH hot-reloads that file
|
|
82
|
+
(`watchUserPatches`) — no restart needed; a failed reload rolls back transactionally. Verify with
|
|
83
|
+
`dsh --profile web --dump-config` or the loader log line `apply plugin dupguard`; remove the
|
|
84
|
+
`insert` entry to uninstall.
|
|
85
|
+
|
|
86
|
+
**Current state on this machine**: the `profiles/web` row still points at the repo's
|
|
87
|
+
`lib/index.js` via a `file:` URL (pre-publish transitional form). To switch to the npm package,
|
|
88
|
+
run `pnpm add dsh-dupguard` in `$DSH_HOME/profiles/web` and change that row's `name` to
|
|
89
|
+
`dsh-dupguard`. The `file:` URL form also remains handy for local development against
|
|
90
|
+
[`lib/index.js`](lib/index.js) (CJS, `module.exports = { name, apply }`, compatible with the
|
|
91
|
+
loader's `unwrapExports`, no build step).
|
|
52
92
|
|
|
53
93
|
---
|
|
54
94
|
|
|
@@ -65,8 +105,10 @@ Edit the `CONFIG` block at the top of `plugin/host.js` / `lib/index.js` (both en
|
|
|
65
105
|
| `maxUnitLength` | `80` | 最大重复单元长度 / maximum repeating-unit length |
|
|
66
106
|
| `detectionWindow` | `8192` | 检测滚动窗口(字符,去空白后)/ rolling detection window in chars (after whitespace removal) |
|
|
67
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 |
|
|
68
109
|
| `monitorReasoning` | `false` | 是否检测思考文本 / also guard reasoning (thinking) text — off by default, high false-positive risk |
|
|
69
110
|
| `monitorToolArguments` | `false` | 是否检测工具调用参数 / also guard tool-call JSON args — off by default (base64/JSON repeats are common) |
|
|
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 |
|
|
70
112
|
|
|
71
113
|
---
|
|
72
114
|
|
|
@@ -120,8 +162,9 @@ agent-loop commits the partial text as a normal assistant message.
|
|
|
120
162
|
| 前缀后循环 / loop after prefix | `好的,下面开始回答:` + `循环` ×10 |
|
|
121
163
|
|
|
122
164
|
**不会触发 / Won't trigger**:正常文本中的高频词(检测只针对**连续**重复)、重复 9 次及以下、
|
|
123
|
-
reasoning
|
|
124
|
-
≤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).
|
|
125
168
|
|
|
126
169
|
---
|
|
127
170
|
|
|
@@ -134,7 +177,8 @@ reasoning 与工具参数(默认关闭)。/ high-frequency words in normal p
|
|
|
134
177
|
├── lib/
|
|
135
178
|
│ └── index.js # npm/组合常驻形式(package.json main 入口)
|
|
136
179
|
├── tests/
|
|
137
|
-
│
|
|
180
|
+
│ ├── detector.test.js # 端到端测试:15 项 × 2 入口(防漂移)
|
|
181
|
+
│ └── experiment-cancel.mjs # 诊断实验(不进 CI):验证截停不阻塞于底层流取消
|
|
138
182
|
├── .github/workflows/ci.yml # GitHub Actions:Node 18/20/22
|
|
139
183
|
├── package.json
|
|
140
184
|
├── CHANGELOG.md
|
|
@@ -164,6 +208,37 @@ Node 18/20/22.
|
|
|
164
208
|
- 服务端停止依赖适配器在流关闭时中止底层请求的语义(已验证 `dsh-llm-deepseek`;自定义适配器需自查)。
|
|
165
209
|
- 阈值语义为 `>= threshold`:第 10 次重复出现时即停止。
|
|
166
210
|
|
|
211
|
+
### DSH 运行期间编辑 preset 后的 standing-mount 冲突(DSH ≤ 0.1.1-rc.1 缺陷,本插件已内置补丁)
|
|
212
|
+
|
|
213
|
+
**现象**:对某个会话执行模型选择等操作时报
|
|
214
|
+
`resume failed ... preset ... failed to mount ... Host Cordis inspect provider "Service" is already registered`,
|
|
215
|
+
此后该错误持续出现,只有**重启 DSH** 才能恢复。
|
|
216
|
+
|
|
217
|
+
**机制**:preset 以 standing mount 方式**每 preset 挂载一次**并常驻;当 preset 的 composition 文件在
|
|
218
|
+
DSH 运行期间被编辑过(mtime/size 变化),下一次对"无活跃 agent 的会话"的操作(模型切换、打开历史会话等)
|
|
219
|
+
会**新建一代 standing mount**,而**旧代从不销毁**(DSH 注释明示 "a superseded one is never disposed
|
|
220
|
+
while the process lives")。`tool-cordis` 在每次挂载时向**进程全局**的 `cordisInspect` 注册表注册
|
|
221
|
+
`Service`/`Event`/`Builtin`/`Tool` 四个 provider,新旧两代并存即冲突;失败的新代回滚、旧代残留,
|
|
222
|
+
重试永远重复冲突——这正是报错后"必须重启才能恢复"的原因。
|
|
223
|
+
|
|
224
|
+
**本插件的修复(默认开启)**:`apply` 时把 `cordisInspect.register` **幂等化**——同 id 已有注册时
|
|
225
|
+
共享既有注册并返回 no-op disposer,多代并存不再冲突。补丁进程内常驻(卸载本插件后仍生效,
|
|
226
|
+
重启后由本插件重新安装;HMR 重载不会叠加)。依赖 `cordisInspect.providers` 为可读 Map
|
|
227
|
+
(rc.6 / rc.7 / 0.1.1-rc.1 实测如此);DSH 升级修复后可将 `CONFIG.fixStandingMountConflict` 置为 `false` 关闭。
|
|
228
|
+
|
|
229
|
+
**仍建议的操作纪律**:运行期间编辑已挂载 preset 后重启 DSH(补丁消除的是报错,旧代残留的
|
|
230
|
+
组合仍占用资源,这是 DSH 的既有行为);根治仍待上游修复。
|
|
231
|
+
|
|
232
|
+
If you edit a mounted preset's `agent.cordis.yml` while DSH is running, the next session resume
|
|
233
|
+
(triggered e.g. by the model picker on a session whose agent is gone) mounts a NEW standing-mount
|
|
234
|
+
generation of that preset while the old generation is never disposed — `tool-cordis` then registers
|
|
235
|
+
its process-global Host inspect providers (`Service` …) twice and every retry fails with
|
|
236
|
+
`Host Cordis inspect provider "Service" is already registered` until DSH restarts. **This plugin
|
|
237
|
+
patches it by default**: `cordisInspect.register` is made idempotent (a same-id registration shares
|
|
238
|
+
the existing one and gets a no-op disposer), so coexisting generations no longer collide. The patch
|
|
239
|
+
is process-resident (survives plugin unload, reinstalled on restart; HMR reload does not stack it);
|
|
240
|
+
set `CONFIG.fixStandingMountConflict` to `false` once a fixed DSH ships.
|
|
241
|
+
|
|
167
242
|
## License
|
|
168
243
|
|
|
169
244
|
[MIT](LICENSE)
|
package/lib/index.js
CHANGED
|
@@ -28,11 +28,24 @@ 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,
|
|
40
|
+
// DSH ≤ 0.1.1-rc.1 兼容补丁(默认开启;实测 0.1.1-rc.1 仍未修复):preset 的 standing mount
|
|
41
|
+
// 在 composition 文件变化后
|
|
42
|
+
// 会新建一代而旧代永不销毁,tool-cordis 每次挂载都向进程全局的 cordisInspect 注册表
|
|
43
|
+
// 注册 Service/Event/Builtin/Tool provider,两代并存即抛
|
|
44
|
+
// "Host Cordis inspect provider ... is already registered"(表现为:截停后模型操作报
|
|
45
|
+
// resume failed,且必须重启 DSH 才能恢复)。开启后本插件把 cordisInspect.register
|
|
46
|
+
// 幂等化(同 id 已注册时共享注册),消除多代冲突。补丁进程内常驻(卸载本插件后
|
|
47
|
+
// 依然生效,重启 DSH 后由本插件重新安装);DSH 升级修复后可关闭。
|
|
48
|
+
fixStandingMountConflict: true,
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
/**
|
|
@@ -68,6 +81,24 @@ function stripWhitespace(text) {
|
|
|
68
81
|
return text.replace(/\s+/g, '')
|
|
69
82
|
}
|
|
70
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
|
+
|
|
71
102
|
/**
|
|
72
103
|
* 为一次 llm/stream 调用创建守卫。
|
|
73
104
|
* 每次模型调用都会新建一份状态,互不干扰。
|
|
@@ -99,7 +130,7 @@ function createStreamGuard(options) {
|
|
|
99
130
|
/** 把一段文本增量喂给检测缓冲,返回命中结果(null 表示未命中)。 */
|
|
100
131
|
function feedText(b, delta) {
|
|
101
132
|
b.text += delta
|
|
102
|
-
const piece =
|
|
133
|
+
const piece = sanitizeDelta(delta, CONFIG)
|
|
103
134
|
b.stripped = (b.stripped + piece).slice(-CONFIG.detectionWindow)
|
|
104
135
|
return findRepeatedTail(b.stripped, CONFIG.threshold, CONFIG.minUnitLength, CONFIG.maxUnitLength)
|
|
105
136
|
}
|
|
@@ -132,7 +163,8 @@ function createStreamGuard(options) {
|
|
|
132
163
|
if (chunk.name !== undefined) b.toolCallName = chunk.name
|
|
133
164
|
b.toolCallArguments += chunk.argumentsDelta
|
|
134
165
|
if (CONFIG.monitorToolArguments) {
|
|
135
|
-
|
|
166
|
+
const piece = sanitizeDelta(chunk.argumentsDelta, CONFIG)
|
|
167
|
+
b.stripped = (b.stripped + piece).slice(-CONFIG.detectionWindow)
|
|
136
168
|
const hit = findRepeatedTail(b.stripped, CONFIG.threshold, CONFIG.minUnitLength, CONFIG.maxUnitLength)
|
|
137
169
|
if (hit !== null) stopped = hit
|
|
138
170
|
}
|
|
@@ -205,7 +237,39 @@ function createStreamGuard(options) {
|
|
|
205
237
|
return guarded
|
|
206
238
|
}
|
|
207
239
|
|
|
240
|
+
/**
|
|
241
|
+
* DSH ≤ 0.1.1-rc.1 standing-mount 兼容补丁:把 cordisInspect.register 幂等化。
|
|
242
|
+
*
|
|
243
|
+
* 背景:preset 的 standing mount 在 composition 文件变化后新建一代、旧代永不销毁;
|
|
244
|
+
* tool-cordis 每次挂载都向进程全局的 cordisInspect 注册 Service/Event/Builtin/Tool
|
|
245
|
+
* provider,两代并存即抛 "already registered"。幂等化后同 id 的后续注册共享已有
|
|
246
|
+
* 注册(返回 no-op disposer),多代并存不再冲突。
|
|
247
|
+
*
|
|
248
|
+
* 说明:补丁有意不做撤销(常驻进程,重启后由本插件重新安装);依赖
|
|
249
|
+
* cordisInspect.providers 为可读 Map(实测 rc.6 / rc.7 / 0.1.1-rc.1 如此)。
|
|
250
|
+
* DSH 升级修复后可将 CONFIG.fixStandingMountConflict 置为 false。
|
|
251
|
+
*/
|
|
252
|
+
function installStandingMountPatch(ctx) {
|
|
253
|
+
const inspect = ctx.get('cordisInspect')
|
|
254
|
+
if (inspect === undefined || typeof inspect.register !== 'function') return
|
|
255
|
+
if (inspect.register.__dupguardIdempotent === true) return
|
|
256
|
+
const original = inspect.register.bind(inspect)
|
|
257
|
+
const patched = (registration) => {
|
|
258
|
+
const id = registration && typeof registration === 'object' ? registration.manifest?.id : undefined
|
|
259
|
+
if (typeof id === 'string' && inspect.providers !== undefined && inspect.providers.has(id)) {
|
|
260
|
+
// 同 id 已有注册(旧代仍活着):共享它,返回 no-op disposer,
|
|
261
|
+
// 新代卸载时不得注销共享注册。
|
|
262
|
+
return () => {}
|
|
263
|
+
}
|
|
264
|
+
return original(registration)
|
|
265
|
+
}
|
|
266
|
+
patched.__dupguardIdempotent = true
|
|
267
|
+
inspect.register = patched
|
|
268
|
+
console.log('[dupguard] cordisInspect.register 已幂等化(DSH standing-mount 多代并存兼容补丁)')
|
|
269
|
+
}
|
|
270
|
+
|
|
208
271
|
function apply(ctx) {
|
|
272
|
+
if (CONFIG.fixStandingMountConflict) installStandingMountPatch(ctx)
|
|
209
273
|
// llm/stream:包裹每次流式模型调用的瀑布事件。
|
|
210
274
|
// 监听器返回包装后的 AsyncIterable,即成为本次调用对消费方可见的流。
|
|
211
275
|
// (与 @deepseek-ai/dsh-llm 的 invariant、dsh-session-checkpoint-policy 同款接入方式)
|
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,11 +25,24 @@ 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,
|
|
37
|
+
// DSH ≤ 0.1.1-rc.1 兼容补丁(默认开启;实测 0.1.1-rc.1 仍未修复):preset 的 standing mount
|
|
38
|
+
// 在 composition 文件变化后
|
|
39
|
+
// 会新建一代而旧代永不销毁,tool-cordis 每次挂载都向进程全局的 cordisInspect 注册表
|
|
40
|
+
// 注册 Service/Event/Builtin/Tool provider,两代并存即抛
|
|
41
|
+
// "Host Cordis inspect provider ... is already registered"(表现为:截停后模型操作报
|
|
42
|
+
// resume failed,且必须重启 DSH 才能恢复)。开启后本插件把 cordisInspect.register
|
|
43
|
+
// 幂等化(同 id 已注册时共享注册),消除多代冲突。补丁进程内常驻(卸载本插件后
|
|
44
|
+
// 依然生效,重启 DSH 后由本插件重新安装);DSH 升级修复后可关闭。
|
|
45
|
+
fixStandingMountConflict: true,
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
/**
|
|
@@ -65,6 +78,24 @@ function stripWhitespace(text) {
|
|
|
65
78
|
return text.replace(/\s+/g, '')
|
|
66
79
|
}
|
|
67
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
|
+
|
|
68
99
|
/**
|
|
69
100
|
* 为一次 llm/stream 调用创建守卫。
|
|
70
101
|
* 每次模型调用都会新建一份状态,互不干扰。
|
|
@@ -96,7 +127,7 @@ function createStreamGuard(options) {
|
|
|
96
127
|
/** 把一段文本增量喂给检测缓冲,返回命中结果(null 表示未命中)。 */
|
|
97
128
|
function feedText(b, delta) {
|
|
98
129
|
b.text += delta
|
|
99
|
-
const piece =
|
|
130
|
+
const piece = sanitizeDelta(delta, CONFIG)
|
|
100
131
|
b.stripped = (b.stripped + piece).slice(-CONFIG.detectionWindow)
|
|
101
132
|
return findRepeatedTail(b.stripped, CONFIG.threshold, CONFIG.minUnitLength, CONFIG.maxUnitLength)
|
|
102
133
|
}
|
|
@@ -129,7 +160,8 @@ function createStreamGuard(options) {
|
|
|
129
160
|
if (chunk.name !== undefined) b.toolCallName = chunk.name
|
|
130
161
|
b.toolCallArguments += chunk.argumentsDelta
|
|
131
162
|
if (CONFIG.monitorToolArguments) {
|
|
132
|
-
|
|
163
|
+
const piece = sanitizeDelta(chunk.argumentsDelta, CONFIG)
|
|
164
|
+
b.stripped = (b.stripped + piece).slice(-CONFIG.detectionWindow)
|
|
133
165
|
const hit = findRepeatedTail(b.stripped, CONFIG.threshold, CONFIG.minUnitLength, CONFIG.maxUnitLength)
|
|
134
166
|
if (hit !== null) stopped = hit
|
|
135
167
|
}
|
|
@@ -202,8 +234,40 @@ function createStreamGuard(options) {
|
|
|
202
234
|
return guarded
|
|
203
235
|
}
|
|
204
236
|
|
|
237
|
+
/**
|
|
238
|
+
* DSH ≤ 0.1.1-rc.1 standing-mount 兼容补丁:把 cordisInspect.register 幂等化。
|
|
239
|
+
*
|
|
240
|
+
* 背景:preset 的 standing mount 在 composition 文件变化后新建一代、旧代永不销毁;
|
|
241
|
+
* tool-cordis 每次挂载都向进程全局的 cordisInspect 注册 Service/Event/Builtin/Tool
|
|
242
|
+
* provider,两代并存即抛 "already registered"。幂等化后同 id 的后续注册共享已有
|
|
243
|
+
* 注册(返回 no-op disposer),多代并存不再冲突。
|
|
244
|
+
*
|
|
245
|
+
* 说明:补丁有意不做撤销(常驻进程,重启后由本插件重新安装);依赖
|
|
246
|
+
* cordisInspect.providers 为可读 Map(实测 rc.6 / rc.7 / 0.1.1-rc.1 如此)。
|
|
247
|
+
* DSH 升级修复后可将 CONFIG.fixStandingMountConflict 置为 false。
|
|
248
|
+
*/
|
|
249
|
+
function installStandingMountPatch(ctx) {
|
|
250
|
+
const inspect = ctx.get('cordisInspect')
|
|
251
|
+
if (inspect === undefined || typeof inspect.register !== 'function') return
|
|
252
|
+
if (inspect.register.__dupguardIdempotent === true) return
|
|
253
|
+
const original = inspect.register.bind(inspect)
|
|
254
|
+
const patched = (registration) => {
|
|
255
|
+
const id = registration && typeof registration === 'object' ? registration.manifest?.id : undefined
|
|
256
|
+
if (typeof id === 'string' && inspect.providers !== undefined && inspect.providers.has(id)) {
|
|
257
|
+
// 同 id 已有注册(旧代仍活着):共享它,返回 no-op disposer,
|
|
258
|
+
// 新代卸载时不得注销共享注册。
|
|
259
|
+
return () => {}
|
|
260
|
+
}
|
|
261
|
+
return original(registration)
|
|
262
|
+
}
|
|
263
|
+
patched.__dupguardIdempotent = true
|
|
264
|
+
inspect.register = patched
|
|
265
|
+
console.log('[dupguard] cordisInspect.register 已幂等化(DSH standing-mount 多代并存兼容补丁)')
|
|
266
|
+
}
|
|
267
|
+
|
|
205
268
|
return {
|
|
206
269
|
apply(ctx) {
|
|
270
|
+
if (CONFIG.fixStandingMountConflict) installStandingMountPatch(ctx)
|
|
207
271
|
// llm/stream:包裹每次流式模型调用的瀑布事件。
|
|
208
272
|
// 监听器返回包装后的 AsyncIterable,即成为本次调用对消费方可见的流。
|
|
209
273
|
// (与 @deepseek-ai/dsh-llm 的 invariant、dsh-session-checkpoint-policy 同款接入方式)
|