@goodandready/dsh-dsml-artifact-guard 0.1.1 → 0.1.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/README.md +133 -35
- package/docs/README.ru.md +152 -0
- package/docs/README.zh.md +152 -0
- package/package.json +28 -1
- package/test/index.test.js +0 -1
package/README.md
CHANGED
|
@@ -1,54 +1,152 @@
|
|
|
1
|
-
# dsh-dsml-artifact-guard
|
|
1
|
+
# 📦 @goodandready/dsh-dsml-artifact-guard
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<div align="center">
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<h3>Fail-Open Stream Sanitizer for Leaked Protocol DSML Closing Tags in DeepSeek Harness</h3>
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-dsml-artifact-guard"><img src="https://img.shields.io/npm/v/@goodandready/dsh-dsml-artifact-guard.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/GooDAnDReaDY/dsh-dsml-artifact-guard.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
+
</p>
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/All_Author_Projects-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="GoodAndReady Showcase"></a>
|
|
16
|
+
</p>
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
20
|
+
<a href="docs/README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
21
|
+
<a href="docs/README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
22
|
+
</p>
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
</div>
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
`sanitize` (remove the terminal artifact), or `disabled`;
|
|
22
|
-
- `providerId`: provider identifier to inspect (default `opencode-go`);
|
|
23
|
-
- `modelId`: model identifier to inspect (default `deepseek-v4-flash`).
|
|
26
|
+
---
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
unchanged. The sanitizer is fail-open: malformed or non-text chunks are
|
|
27
|
-
forwarded, and only a terminal closing-tag sequence is considered an artifact.
|
|
28
|
+
## ⚡ Overview & The Problem
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
When interacting with certain upstream model providers or API gateways, raw DSML (DeepSeek Markup Language) tool-invocation protocol tags can leak into the assistant's visible text stream. Users frequently see trailing protocol clutter like:
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
```text
|
|
33
|
+
Done. All tests have passed.
|
|
34
|
+
</|DSML|parameter> </|DSML|invoke> </|DSML|tool_calls>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
These leaked closing tags visually pollute the chat bubble, cause Markdown rendering glitches, and can confuse downstream agents or clipboard exports.
|
|
38
|
+
|
|
39
|
+
**`@goodandready/dsh-dsml-artifact-guard`** is a lightweight, host-only runtime stream interceptor for DeepSeek Harness that cleans up these terminal artifacts in real time before they reach the user interface:
|
|
40
|
+
|
|
41
|
+
1. **Synchronous Stream Contract Preservation**: Cordis requires stream interceptors to return an `AsyncIterable` synchronously. Making interceptors `async` returns a `Promise` that crashes the harness turn with `stream is not async iterable`. This guard adheres strictly to the synchronous hook contract.
|
|
42
|
+
2. **Split Chunk Buffer Pipeline**: Protocol tags often arrive split across multiple TCP or WebSocket text deltas. The guard maintains a small sliding buffer (`KEEP = 96` bytes) to reliably match and strip multi-chunk tails.
|
|
43
|
+
3. **100% Fail-Open Safety**: Never drops legitimate user or assistant text. Legitimate discussions about DSML syntax or internal tool calls are preserved intact.
|
|
44
|
+
4. **Targeted Provider & Model Scoping**: Restricts processing specifically to the provider and model configurations that exhibit tag leakage, passing other model traffic through with zero overhead.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 🏗️ Architecture
|
|
49
|
+
|
|
50
|
+
```mermaid
|
|
51
|
+
graph TD
|
|
52
|
+
subgraph DSH ["DeepSeek Harness Runtime"]
|
|
53
|
+
Turn["Agent Turn Execution<br/>(LLM Stream Request)"]
|
|
54
|
+
ChatUI["Chat UI Stream Consumer<br/>(Renders clean markdown text)"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
subgraph Guard ["@goodandready/dsh-dsml-artifact-guard"]
|
|
58
|
+
Hook["Synchronous llm/stream Hook<br/>(Returns AsyncIterable synchronously)"]
|
|
59
|
+
ScopeCheck{"Scope Match?<br/>(providerId & modelId)"}
|
|
60
|
+
PassThrough["Raw Stream Pass-Through<br/>(Zero overhead for other models)"]
|
|
61
|
+
Buffer["Sliding Tail Buffer<br/>(Preserves trailing 96 bytes across deltas)"]
|
|
62
|
+
Detector{"Terminal Artifact?<br/>(Matches leaked DSML tail at finish)"}
|
|
63
|
+
Sanitize["Sanitize Mode<br/>(Strips leaked closing tags)"]
|
|
64
|
+
Audit["Audit Mode<br/>(Emits ctx.logger warning only)"]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
Turn -->|ctx.on('llm/stream')| Hook
|
|
68
|
+
Hook --> ScopeCheck
|
|
69
|
+
ScopeCheck -->|No| PassThrough
|
|
70
|
+
ScopeCheck -->|Yes| Buffer
|
|
71
|
+
PassThrough --> ChatUI
|
|
72
|
+
Buffer --> Detector
|
|
73
|
+
Detector -->|No Artifact| ChatUI
|
|
74
|
+
Detector -->|Artifact Found & sanitize| Sanitize --> ChatUI
|
|
75
|
+
Detector -->|Artifact Found & audit| Audit --> ChatUI
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## ✨ Features & Capabilities
|
|
81
|
+
|
|
82
|
+
### 1. Synchronous Hook Guarantee
|
|
83
|
+
Under Cordis and DSH service lifecycles, event listeners on `llm/stream` must return the transformed stream synchronously. An asynchronous hook wrapper will return a `Promise<AsyncIterable>`, causing the runtime dispatcher to immediately throw `TypeError: stream is not async iterable`. `dsh-dsml-artifact-guard` wraps the stream generator in a pure synchronous registration.
|
|
84
|
+
|
|
85
|
+
### 2. Multi-Chunk Tail Buffering
|
|
86
|
+
In real-world streaming, the artifact `</|DSML|parameter> </|DSML|invoke> </|DSML|tool_calls>` is frequently fractured into fragments:
|
|
87
|
+
* Chunk 1: `All tasks complete. </|DSML|pa`
|
|
88
|
+
* Chunk 2: `rameter> </|DSML|invoke> `
|
|
89
|
+
* Chunk 3: `</|DSML|tool_calls>`
|
|
34
90
|
|
|
35
|
-
|
|
91
|
+
The guard retains a minimal 96-byte window until the next chunk or `finish` event arrives, ensuring fractured tags are seamlessly detected and sanitized as a single terminal artifact.
|
|
92
|
+
|
|
93
|
+
### 3. Fail-Open Architecture
|
|
94
|
+
* If the text contains genuine prose about DSML (e.g. `<|DSML|tool_calls>example</|DSML|tool_calls>`), it is **never** removed.
|
|
95
|
+
* Non-text chunks (`tool-call-delta`, `usage`, `finish`) are forwarded immediately without delay.
|
|
96
|
+
* Any malformed chunk structure passes through transparently to preserve session stability.
|
|
97
|
+
|
|
98
|
+
### 4. Flexible Operating Modes
|
|
99
|
+
* **`sanitize`** *(default)*: Strips terminal DSML closing tags and logs a warning with the count of removed artifacts.
|
|
100
|
+
* **`audit`**: Emits diagnostic logs with `ctx.logger.info(...)` without modifying the user-visible stream.
|
|
101
|
+
* **`disabled`**: Bypasses processing entirely.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 📦 Installation
|
|
106
|
+
|
|
107
|
+
Install into your DeepSeek Harness web profile:
|
|
36
108
|
|
|
37
109
|
```bash
|
|
38
|
-
|
|
39
|
-
npm run check
|
|
40
|
-
npm pack --dry-run --json
|
|
110
|
+
dsh plugin --profile web add @goodandready/dsh-dsml-artifact-guard
|
|
41
111
|
```
|
|
42
112
|
|
|
43
|
-
|
|
44
|
-
matching, and the synchronous Cordis hook contract.
|
|
113
|
+
Restart your DeepSeek Harness instance.
|
|
45
114
|
|
|
46
|
-
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## ⚙️ Configuration (`settings.yaml`)
|
|
118
|
+
|
|
119
|
+
Configure provider and model targets in `settings.yaml` or through the Web UI:
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
# settings.yaml
|
|
123
|
+
dsh-dsml-artifact-guard:
|
|
124
|
+
mode: sanitize
|
|
125
|
+
providerId: "your-provider-id"
|
|
126
|
+
modelId: "your-model-id"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Configuration Parameters
|
|
130
|
+
|
|
131
|
+
| Parameter | Type | Default | Description |
|
|
132
|
+
|:---|:---|:---|:---|
|
|
133
|
+
| `mode` | `string` | `"sanitize"` | Operation mode: `"sanitize"` (strip tags), `"audit"` (log only), or `"disabled"` |
|
|
134
|
+
| `providerId` | `string` | `"opencode-go"` | Target provider identifier exhibiting leaked tags |
|
|
135
|
+
| `modelId` | `string` | `"deepseek-v4-flash"` | Target model identifier exhibiting leaked tags |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 🧪 Testing
|
|
140
|
+
|
|
141
|
+
Run the automated test suite covering split chunks, audit vs sanitize modes, scope matching, and synchronous hook contracts:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm test
|
|
145
|
+
npm run check
|
|
146
|
+
```
|
|
47
147
|
|
|
48
|
-
|
|
49
|
-
- Gitea issue #6: package license, ignore rules, and deterministic package
|
|
50
|
-
contents.
|
|
148
|
+
---
|
|
51
149
|
|
|
52
|
-
##
|
|
150
|
+
## 📄 License
|
|
53
151
|
|
|
54
|
-
|
|
152
|
+
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# 📦 @goodandready/dsh-dsml-artifact-guard
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<h3>Предохранитель и потоковый очиститель от артефактов закрывающих тегов протокола DSML в DeepSeek Harness</h3>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-dsml-artifact-guard"><img src="https://img.shields.io/npm/v/@goodandready/dsh-dsml-artifact-guard.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/GooDAnDReaDY/dsh-dsml-artifact-guard.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/Все_проекты_автора-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="Все проекты автора"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="../README.md"><b>🇬🇧 English</b></a> •
|
|
20
|
+
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
21
|
+
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## ⚡ Назначение и решаемая проблема
|
|
29
|
+
|
|
30
|
+
При взаимодействии с некоторыми сторонними провайдерами моделей или прокси-шлюзами служебные теги протокола вызова инструментов DSML (DeepSeek Markup Language) могут просачиваться в видимый текстовый поток ассистента. В конце ответов пользователи нередко наблюдают протокольный мусор вида:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
Готово. Все тесты успешно пройдены.
|
|
34
|
+
</|DSML|parameter> </|DSML|invoke> </|DSML|tool_calls>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Такие теги загрязняют чат, ломают рендеринг Markdown и сбивают парсинг при копировании или передаче ответов другим агентам.
|
|
38
|
+
|
|
39
|
+
**`@goodandready/dsh-dsml-artifact-guard`** — легковесный плагин времени выполнения (host-only) для DeepSeek Harness, перехватывающий и удаляющий эти паразитные терминальные артефакты в реальном времени до того, как текст попадёт в интерфейс:
|
|
40
|
+
|
|
41
|
+
1. **Строго синхронный контракт хука**: в среде Cordis обработчик события `llm/stream` обязан синхронно возвращать итератор `AsyncIterable`. Сделать обработчик асинхронным (`async`) означает вернуть `Promise`, что неизбежно приводит к падению среды выполнения с ошибкой `stream is not async iterable`. Плагин строго соблюдает синхронный контракт.
|
|
42
|
+
2. **Буферизация фрагментированного хвоста**: сетевые чанки часто разрезают закрывающие теги на части. Плагин использует скользящее окно размером 96 байт (`KEEP = 96`), гарантируя надёжный захват и вырезание артефакта, даже если он разделен между пакетами.
|
|
43
|
+
3. **100% Fail-Open архитектура**: плагин никогда не удаляет полезный пользовательский текст или легитимные обсуждения синтаксиса DSML.
|
|
44
|
+
4. **Адресный скоупинг (Provider & Model)**: фильтрация применяется только к целевым провайдерам и моделям, у которых наблюдается утечка протокольных тегов. Трафик остальных моделей проходит без задержек и оверхеда.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 🏗️ Архитектура работы
|
|
49
|
+
|
|
50
|
+
```mermaid
|
|
51
|
+
graph TD
|
|
52
|
+
subgraph DSH ["Среда DeepSeek Harness"]
|
|
53
|
+
Turn["Ход выполнения агента<br/>(Запрос к потоку модели)"]
|
|
54
|
+
ChatUI["Отображение в чате<br/>(Чистый текст без паразитных тегов)"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
subgraph Guard ["@goodandready/dsh-dsml-artifact-guard"]
|
|
58
|
+
Hook["Синхронный хук llm/stream<br/>(Возвращает AsyncIterable синхронно)"]
|
|
59
|
+
ScopeCheck{"Совпадает скоуп?<br/>(providerId и modelId)"}
|
|
60
|
+
PassThrough["Сквозной пропуск потока<br/>(Нулевой оверхед для других моделей)"]
|
|
61
|
+
Buffer["Буфер скользящего хвоста<br/>(Сохраняет последние 96 байт)"]
|
|
62
|
+
Detector{"Терминальный артефакт?<br/>(Поиск хвоста DSML при finish)"}
|
|
63
|
+
Sanitize["Режим Sanitize<br/>(Вырезание закрывающих тегов)"]
|
|
64
|
+
Audit["Режим Audit<br/>(Логирование через ctx.logger)"]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
Turn -->|ctx.on('llm/stream')| Hook
|
|
68
|
+
Hook --> ScopeCheck
|
|
69
|
+
ScopeCheck -->|Нет| PassThrough
|
|
70
|
+
ScopeCheck -->|Да| Buffer
|
|
71
|
+
PassThrough --> ChatUI
|
|
72
|
+
Buffer --> Detector
|
|
73
|
+
Detector -->|Нет артефакта| ChatUI
|
|
74
|
+
Detector -->|Артефакт найден & sanitize| Sanitize --> ChatUI
|
|
75
|
+
Detector -->|Артефакт найден & audit| Audit --> ChatUI
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## ✨ Подробный разбор возможностей
|
|
81
|
+
|
|
82
|
+
### 1. Защита синхронного контракта Cordis
|
|
83
|
+
В архитектуре Cordis вызов `ctx.on('llm/stream', (o, next) => ...)` ожидает немедленный возврат асинхронного генератора. Если функция хука объявлена как `async`, JavaScript оборачивает возвращаемое значение в `Promise`. В результате потребители стрима падают с фатальной ошибкой `TypeError: stream is not async iterable`. `dsh-dsml-artifact-guard` оборачивает поток синхронно, сохраняя целостность рантайма.
|
|
84
|
+
|
|
85
|
+
### 2. Захват разорванных сетевых чанков
|
|
86
|
+
В реальном сетевом потоке завершающая последовательность тегов может приходить отдельными фрагментами:
|
|
87
|
+
* Чанк 1: `Задача выполнена. </|DSML|pa`
|
|
88
|
+
* Чанк 2: `rameter> </|DSML|invoke> `
|
|
89
|
+
* Чанк 3: `</|DSML|tool_calls>`
|
|
90
|
+
|
|
91
|
+
Буфер скользящего хвоста удерживает последние 96 байт до получения следующего текстового блока или сигнала завершения `finish`, точно сопоставляя полный шаблон и удаляя его как единое целое.
|
|
92
|
+
|
|
93
|
+
### 3. Гарантия сохранности данных (Fail-Open)
|
|
94
|
+
* Если в ответе содержится обычный текст с упоминанием тегов (например, руководство по написанию `<|DSML|tool_calls>`), текст **никогда** не удаляется.
|
|
95
|
+
* Нетекстовые чанки (`tool-call-delta`, `usage`, `finish`) пробрасываются немедленно без задержек.
|
|
96
|
+
* При любых непредвиденных ошибках или нестандартных форматах чанков поток не прерывается, а беспрепятственно передаётся дальше.
|
|
97
|
+
|
|
98
|
+
### 4. Режимы работы
|
|
99
|
+
* **`sanitize`** *(по умолчанию)*: вырезает паразитные закрывающие теги и фиксирует событие в журнале с указанием числа удалённых артефактов.
|
|
100
|
+
* **`audit`**: только логирует обнаружение артефактов через `ctx.logger.info(...)`, не модифицируя текст в чате.
|
|
101
|
+
* **`disabled`**: полностью отключает обработку.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 📦 Установка
|
|
106
|
+
|
|
107
|
+
Установка через CLI DeepSeek Harness:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
dsh plugin --profile web add @goodandready/dsh-dsml-artifact-guard
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Перезапустите экземпляр DeepSeek Harness.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## ⚙️ Конфигурация (`settings.yaml`)
|
|
118
|
+
|
|
119
|
+
Настройка провайдера и модели в `settings.yaml` или через веб-панель управления:
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
# settings.yaml
|
|
123
|
+
dsh-dsml-artifact-guard:
|
|
124
|
+
mode: sanitize
|
|
125
|
+
providerId: "your-provider-id"
|
|
126
|
+
modelId: "your-model-id"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Таблица параметров конфигурации
|
|
130
|
+
|
|
131
|
+
| Параметр | Тип | По умолчанию | Описание |
|
|
132
|
+
|:---|:---|:---|:---|
|
|
133
|
+
| `mode` | `string` | `"sanitize"` | Режим работы: `"sanitize"` (удалять теги), `"audit"` (только логировать) или `"disabled"` |
|
|
134
|
+
| `providerId` | `string` | `"opencode-go"` | Идентификатор целевого провайдера, отдающего паразитные теги |
|
|
135
|
+
| `modelId` | `string` | `"deepseek-v4-flash"` | Идентификатор целевой модели с артефактами |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 🧪 Тестирование
|
|
140
|
+
|
|
141
|
+
Запуск автоматических тестов:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm test
|
|
145
|
+
npm run check
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 📄 Лицензия
|
|
151
|
+
|
|
152
|
+
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# 📦 @goodandready/dsh-dsml-artifact-guard
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<h3>面向 DeepSeek Harness 的残留 DSML 协议闭合标签流式熔断清洗引擎</h3>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-dsml-artifact-guard"><img src="https://img.shields.io/npm/v/@goodandready/dsh-dsml-artifact-guard.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/GooDAnDReaDY/dsh-dsml-artifact-guard.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/作者全部项目-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="作者全部项目"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="../README.md"><b>🇬🇧 English</b></a> •
|
|
20
|
+
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
21
|
+
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## ⚡ 核心定位与解决痛点
|
|
29
|
+
|
|
30
|
+
在对接特定第三方大模型服务商或代理网关时,底层工具调用协议 DSML(DeepSeek Markup Language)的闭合标签有时会异常泄漏并附着在模型输出的正文末尾。用户在聊天界面中经常看到多余的协议残留:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
任务完成,所有测试均已通过。
|
|
34
|
+
</|DSML|parameter> </|DSML|invoke> </|DSML|tool_calls>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
此类闭合标签不仅造成视觉污染、破坏 Markdown 排版渲染,还会干扰后续多智能体协作解析或文本复制导出。
|
|
38
|
+
|
|
39
|
+
**`@goodandready/dsh-dsml-artifact-guard`** 是专为 DeepSeek Harness 打造的原生宿主级(host-only)文本流拦截过滤插件,在文本到达前端界面之前实时侦测并净化残留闭合标签:
|
|
40
|
+
|
|
41
|
+
1. **严格同步 Hook 契约保障**:Cordis 要求流拦截器必须同步返回 `AsyncIterable`。将拦截器写成 `async` 会返回 `Promise`,直接引发运行时崩溃:`stream is not async iterable`。本插件严格保持同步注册。
|
|
42
|
+
2. **分片分包滑动缓冲流**:网络分包常将闭合标签切断并分散在不同 Text Delta 中。插件内置 96 字节(`KEEP = 96`)滑动窗口,精准捕获并合并跨分片的长尾标签。
|
|
43
|
+
3. **100% 故障开放(Fail-Open)安全**:绝不误伤合法正文。对于讨论 DSML 语法的正常技术文本,插件保证原样放行。
|
|
44
|
+
4. **定向模型与服务商匹配**:仅对发生协议泄漏的目标 Provider/Model 执行流式清洗,其他模型流量直通零开销。
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 🏗️ 架构设计
|
|
49
|
+
|
|
50
|
+
```mermaid
|
|
51
|
+
graph TD
|
|
52
|
+
subgraph DSH ["DeepSeek Harness 核心运行时"]
|
|
53
|
+
Turn["智能体回合执行<br/>(发起大模型流式请求)"]
|
|
54
|
+
ChatUI["聊天界面流式消费端<br/>(渲染纯净 Markdown 文本)"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
subgraph Guard ["@goodandready/dsh-dsml-artifact-guard"]
|
|
58
|
+
Hook["同步 llm/stream 拦截钩子<br/>(同步返回 AsyncIterable)"]
|
|
59
|
+
ScopeCheck{"是否命中范围?<br/>(校验 providerId 与 modelId)"}
|
|
60
|
+
PassThrough["原始数据流直通<br/>(其他模型零延迟零开销)"]
|
|
61
|
+
Buffer["滑动尾部缓冲区<br/>(跨分片保留最后 96 字节)"]
|
|
62
|
+
Detector{"末端残留判定?<br/>(流结束 finish 时比对标签)"]
|
|
63
|
+
Sanitize["清洗模式 (Sanitize)<br/>(安全剥除协议残留标签)"]
|
|
64
|
+
Audit["审计模式 (Audit)<br/>(仅记录日志不改动文本)"]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
Turn -->|ctx.on('llm/stream')| Hook
|
|
68
|
+
Hook --> ScopeCheck
|
|
69
|
+
ScopeCheck -->|未命中| PassThrough
|
|
70
|
+
ScopeCheck -->|命中| Buffer
|
|
71
|
+
PassThrough --> ChatUI
|
|
72
|
+
Buffer --> Detector
|
|
73
|
+
Detector -->|无异常| ChatUI
|
|
74
|
+
Detector -->|检出残留 & sanitize| Sanitize --> ChatUI
|
|
75
|
+
Detector -->|检出残留 & audit| Audit --> ChatUI
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## ✨ 核心特性深度解析
|
|
81
|
+
|
|
82
|
+
### 1. 同步 Hook 契约保护
|
|
83
|
+
在 Cordis 与 DSH 的底层事件总线中,`llm/stream` 监听器必须同步返回转换后的迭代器。若将回调函数声明为 `async`,返回值将被自动包裹为 `Promise<AsyncIterable>`,导致下层调度器抛出致命异常:`TypeError: stream is not async iterable`。`dsh-dsml-artifact-guard` 采用纯同步生成器封装,从根源规避调度崩溃。
|
|
84
|
+
|
|
85
|
+
### 2. 跨分片长尾标签精准捕获
|
|
86
|
+
在实际网络传输中,长尾标签 `</|DSML|parameter> </|DSML|invoke> </|DSML|tool_calls>` 常被拆分为多个 chunk:
|
|
87
|
+
* Chunk 1: `All tasks complete. </|DSML|pa`
|
|
88
|
+
* Chunk 2: `rameter> </|DSML|invoke> `
|
|
89
|
+
* Chunk 3: `</|DSML|tool_calls>`
|
|
90
|
+
|
|
91
|
+
滑动窗口在接收到下一个文本分片或 `finish` 信号前保留尾部 96 字节,确保跨包切分的标签被作为一个完整实体精准匹配并剔除。
|
|
92
|
+
|
|
93
|
+
### 3. 数据安全与故障开放保障
|
|
94
|
+
* 正文技术讨论中出现 DSML 示例代码时(如 `<|DSML|tool_calls>demo</|DSML|tool_calls>`),绝不误删。
|
|
95
|
+
* 非文本分片(`tool-call-delta`, `usage`, `finish`)即时透传,无任何额外排队延迟。
|
|
96
|
+
* 遇到畸形数据格式时自动降级放行,优先确保会话畅通不中断。
|
|
97
|
+
|
|
98
|
+
### 4. 三种灵活运行模式
|
|
99
|
+
* **`sanitize`** *(默认)*:自动剥离末尾残留标签,并在日志中输出剥离计数。
|
|
100
|
+
* **`audit`**:仅通过 `ctx.logger.info(...)` 记录检出警告,保持用户界面原样输出。
|
|
101
|
+
* **`disabled`**:完全旁路,不进行任何处理。
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 📦 快速安装
|
|
106
|
+
|
|
107
|
+
通过 DeepSeek Harness CLI 一键安装:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
dsh plugin --profile web add @goodandready/dsh-dsml-artifact-guard
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
重启 DeepSeek Harness 实例即可生效。
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## ⚙️ 配置指南 (`settings.yaml`)
|
|
118
|
+
|
|
119
|
+
在 `settings.yaml` 或 Web UI 设置面板中配置目标服务商与模型:
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
# settings.yaml
|
|
123
|
+
dsh-dsml-artifact-guard:
|
|
124
|
+
mode: sanitize
|
|
125
|
+
providerId: "your-provider-id"
|
|
126
|
+
modelId: "your-model-id"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 配置参数参考表
|
|
130
|
+
|
|
131
|
+
| 参数名 | 类型 | 默认值 | 功能说明 |
|
|
132
|
+
|:---|:---|:---|:---|
|
|
133
|
+
| `mode` | `string` | `"sanitize"` | 运行模式:`"sanitize"`(清洗标签)、`"audit"`(仅审计)或 `"disabled"` |
|
|
134
|
+
| `providerId` | `string` | `"opencode-go"` | 发生协议标签泄漏的目标服务商标识符 |
|
|
135
|
+
| `modelId` | `string` | `"deepseek-v4-flash"` | 发生协议标签泄漏的目标模型标识符 |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 🧪 测试与校验
|
|
140
|
+
|
|
141
|
+
运行全部自动化测试与语法检查:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm test
|
|
145
|
+
npm run check
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 📄 开源许可证
|
|
151
|
+
|
|
152
|
+
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
package/package.json
CHANGED
|
@@ -1 +1,28 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@goodandready/dsh-dsml-artifact-guard",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Sanitizes leaked DeepSeek DSML closing tags from model text streams",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"files": [
|
|
9
|
+
"lib",
|
|
10
|
+
"cordis.patch.yml",
|
|
11
|
+
"README.md",
|
|
12
|
+
"docs",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"check": "node --check lib/index.js",
|
|
17
|
+
"test": "node --test"
|
|
18
|
+
},
|
|
19
|
+
"dsh": {
|
|
20
|
+
"bundle": {
|
|
21
|
+
"patch": "./cordis.patch.yml"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
26
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/test/index.test.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import assert from'node:assert/strict';import test from'node:test';import{apply,matchesScope,sanitizeDsmlArtifacts}from'../lib/index.js';const tail='</|DSML|parameter> </|DSML|invoke> </|DSML|tool_calls>';async function collect(c,o){const r=[];async function*s(){yield*c}for await(const x of sanitizeDsmlArtifacts(s(),o))r.push(x);return r}test('removes terminal tail',async()=>{const c=[{type:'tool-call-delta',index:1},{type:'text-delta',index:2,text:'Done. '+tail},{type:'finish'}];assert.deepEqual(await collect(c,{mode:'sanitize'}),[c[0],{type:'text-delta',index:2,text:'Done. '},c[2]])});test('split tail',async()=>{const c=[{type:'text-delta',index:0,text:'A '+tail.slice(0,20)},{type:'text-delta',index:0,text:tail.slice(20)},{type:'finish'}];assert.deepEqual(await collect(c,{mode:'sanitize'}),[{type:'text-delta',index:0,text:'A '},c[2]])});test('audit unchanged',async()=>{const c=[{type:'text-delta',index:0,text:tail},{type:'finish'}];assert.deepEqual(await collect(c,{mode:'audit'}),c)});test('keeps prose',async()=>{const c=[{type:'text-delta',index:0,text:'<|DSML|tool_calls>x</|DSML|tool_calls>'},{type:'usage'},{type:'finish'}];assert.deepEqual(await collect(c,{mode:'sanitize'}),c)});test('scope',()=>assert.equal(matchesScope({providerId:'opencode-go',modelId:'deepseek-v4-flash'},{providerId:'opencode-go',modelId:'deepseek-v4-flash'}),true));test('llm stream hook returns an async iterable synchronously',()=>{let hook;const ctx={on(_event,fn){hook=fn},logger:{info(){}}};apply(ctx,{mode:'sanitize',providerId:'opencode-go',modelId:'deepseek-v4-flash'});assert.equal(typeof hook,'function');async function*stream(){yield{type:'finish'}}const source=stream();const result=hook({providerId:'opencode-go',modelId:'deepseek-v4-flash'},()=>source);assert.equal(typeof result?.[Symbol.asyncIterator],'function');assert.equal(typeof result?.then,'undefined')});test('out of scope stream passes through unchanged',()=>{let hook;const ctx={on(_event,fn){hook=fn},logger:{info(){}}};apply(ctx,{mode:'sanitize',providerId:'opencode-go',modelId:'deepseek-v4-flash'});async function*stream(){yield{type:'finish'}}const source=stream();assert.equal(hook({providerId:'other',modelId:'model'},()=>source),source)});
|