@goodandready/dsh-dsml-artifact-guard 0.2.7 → 0.2.8

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 CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.2.8] - 2026-09-26
11
+
12
+ ### Added
13
+ - **Universal DeepSeek Model Matching**: replaced strict single `providerId`/`modelId` pair with case-insensitive `modelPattern` RegExp (default: `deepseek`), automatically protecting all DeepSeek models regardless of provider gateway or vendor prefix (#48).
14
+ - **Target Providers Filter**: added optional `providers` array filter (empty = all providers) (#48).
15
+ - **Russian Localization**: added native `ru` translations in the Web UI settings card alongside English and Chinese (#48).
16
+
17
+ ### Changed
18
+ - **De-identification**: removed proprietary `opencode-go` default in public package configuration.
19
+ - **Backward Compatibility**: user configs with legacy `providerId` and `modelId` continue to work as an additional exact rule, with a deprecation notice and one-click clear button in the Web UI settings card.
20
+ - **Documentation**: added dedicated section explaining lack of DSML processing in core DSH 0.1.7-rc.2.
21
+
10
22
  ## [0.2.7] - 2026-09-25
11
23
 
12
24
  ### Fixed
package/README.md CHANGED
@@ -55,6 +55,14 @@ These leaked closing tags visually pollute the chat bubble, cause Markdown rende
55
55
 
56
56
  ---
57
57
 
58
+ ## 💡 Why Not in DSH Core?
59
+
60
+ As of DeepSeek Harness **0.1.7-rc.2**, the core upstream stream pipeline (`dsh-llm-pi-ai`, `dsh-llm-deepseek*`, etc.) does not perform terminal DSML protocol cleanup — there is zero mention or handling of `|DSML|` anywhere in the core engine. When upstream gateways (e.g. `commandcode`, `deepseek-official`, or custom proxies) leak protocol closing tags at the conclusion of an assistant turn, DSH forwards them directly to the frontend.
61
+
62
+ `dsh-dsml-artifact-guard` serves as the universal runtime guard across all DeepSeek models regardless of provider gateway or routing configuration.
63
+
64
+ ---
65
+
58
66
  ## 🏗️ Architecture
59
67
 
60
68
  ```mermaid
@@ -141,14 +149,14 @@ Restart your DeepSeek Harness instance.
141
149
 
142
150
  ## ⚙️ Configuration (`settings.yaml`)
143
151
 
144
- Configure provider and model targets in `settings.yaml` or through the Web UI:
152
+ Configure model matching and provider targets in `settings.yaml` or through the Web UI:
145
153
 
146
154
  ```yaml
147
155
  # settings.yaml
148
156
  dsh-dsml-artifact-guard:
149
157
  mode: sanitize
150
- providerId: "your-provider-id"
151
- modelId: "your-model-id"
158
+ modelPattern: "deepseek" # Case-insensitive RegExp matching all DeepSeek models
159
+ providers: [] # Empty = guard all providers; or e.g. ["commandcode", "deepseek-official"]
152
160
  ```
153
161
 
154
162
  ### Configuration Parameters
@@ -156,8 +164,10 @@ dsh-dsml-artifact-guard:
156
164
  | Parameter | Type | Default | Description |
157
165
  |:---|:---|:---|:---|
158
166
  | `mode` | `string` | `"sanitize"` | Operation mode: `"sanitize"` (strip tags), `"audit"` (log only), or `"disabled"` |
159
- | `providerId` | `string` | `"opencode-go"` | Target provider identifier exhibiting leaked tags |
160
- | `modelId` | `string` | `"deepseek-v4-flash"` | Target model identifier exhibiting leaked tags |
167
+ | `modelPattern` | `string` | `"deepseek"` | Case-insensitive RegExp matched against model IDs (e.g. `deepseek`, `deepseek-v4.*`, `deepseek/.*`) |
168
+ | `providers` | `string[]` | `[]` | Optional list of provider IDs. When empty, guards all providers |
169
+ | `providerId` | `string` | `undefined` | *(Deprecated)* Legacy exact provider identifier; maintained for backward compatibility |
170
+ | `modelId` | `string` | `undefined` | *(Deprecated)* Legacy exact model identifier; maintained for backward compatibility |
161
171
 
162
172
  ### HTTP Management Endpoints
163
173
 
package/README.ru.md CHANGED
@@ -55,6 +55,14 @@
55
55
 
56
56
  ---
57
57
 
58
+ ## 💡 Почему этого нет в ядре DSH?
59
+
60
+ По состоянию на DeepSeek Harness **0.1.7-rc.2**, ядро системы (`dsh-llm-pi-ai`, `dsh-llm-deepseek*` и др.) вообще не выполняет очистку завершающих тегов DSML — в ядровом коде нет ни единого упоминания `|DSML|`. Когда вышестоящие провайдеры (например, `commandcode`, `deepseek-official` или внешние шлюзы) отдают в потоке закрывающие теги протокола, DSH пропускает их прямо в интерфейс чата.
61
+
62
+ Плагин `dsh-dsml-artifact-guard` обеспечивает универсальную защиту для всех моделей DeepSeek независимо от конкретного шлюза или маршрутизатора.
63
+
64
+ ---
65
+
58
66
  ## 🏗️ Архитектура работы
59
67
 
60
68
  ```mermaid
package/README.zh.md CHANGED
@@ -55,6 +55,14 @@
55
55
 
56
56
  ---
57
57
 
58
+ ## 💡 为什么核心层未包含此功能?
59
+
60
+ 截至 DeepSeek Harness **0.1.7-rc.2**,核心上游流管道(`dsh-llm-pi-ai`、`dsh-llm-deepseek*` 等)均未对末端 DSML 协议标签进行任何清洗——核心引擎中完全不存在对 `|DSML|` 的处理。当上游网关(例如 `commandcode`、`deepseek-official` 或其他代理)在回复结尾泄漏协议闭合标签时,DSH 会将其直接传输到聊天前端。
61
+
62
+ `dsh-dsml-artifact-guard` 为所有 DeepSeek 模型提供通用的运行时防护,不受特定服务商网关或路由配置的限制。
63
+
64
+ ---
65
+
58
66
  ## 🏗️ 架构设计
59
67
 
60
68
  ```mermaid
package/lib/client.js CHANGED
@@ -41,8 +41,15 @@ window.__ModuleLoader__.load({
41
41
  modeSanitize: 'Sanitize (strip leaked closing tags)',
42
42
  modeAudit: 'Audit (log warnings only, preserve output)',
43
43
  modeDisabled: 'Disabled (bypass all streams)',
44
- providerId: 'Target Provider ID',
45
- modelId: 'Target Model ID',
44
+ modelPattern: 'Model Pattern (RegExp)',
45
+ modelPatternHint: 'Case-insensitive RegExp matched against model IDs (e.g. deepseek, deepseek-v4.*). Default: deepseek.',
46
+ providers: 'Target Providers (optional)',
47
+ providersPlaceholder: 'all providers (comma-separated, e.g. commandcode, deepseek-official)',
48
+ providersHint: 'Comma-separated list of provider IDs. Leave empty to guard all providers.',
49
+ legacyDetected: 'Legacy providerId / modelId detected: active as an additional rule.',
50
+ legacyClear: 'Clear legacy fields',
51
+ providerId: 'Target Provider ID (legacy)',
52
+ modelId: 'Target Model ID (legacy)',
46
53
  disabledNotice: '⚠️ Guard is currently disabled. DSML closing tags will reach the chat interface.',
47
54
  statusUnavailable: 'Settings service unavailable.',
48
55
  saving: 'Saving…',
@@ -67,8 +74,15 @@ window.__ModuleLoader__.load({
67
74
  modeSanitize: 'Sanitize 清洗(自动剥除残留闭合标签)',
68
75
  modeAudit: 'Audit 审计(仅在日志记录警告,不修改正文)',
69
76
  modeDisabled: 'Disabled 禁用(直通所有数据流)',
70
- providerId: '目标服务商标识 (Provider ID)',
71
- modelId: '目标模型标识 (Model ID)',
77
+ modelPattern: '模型正则匹配式 (RegExp)',
78
+ modelPatternHint: '对模型 ID 进行不区分大小写的正则表达式匹配(默认:deepseek)。支持任意服务商前缀。',
79
+ providers: '目标服务商(可选)',
80
+ providersPlaceholder: '所有服务商(以逗号分隔,例如 commandcode, deepseek-official)',
81
+ providersHint: '以逗号分隔的服务商 ID 列表。留空则保护所有服务商。',
82
+ legacyDetected: '检测到旧版 providerId / modelId 配置,并作为附加精确规则生效。',
83
+ legacyClear: '清除旧版配置',
84
+ providerId: '目标服务商标识 (Provider ID, 旧版)',
85
+ modelId: '目标模型标识 (Model ID, 旧版)',
72
86
  disabledNotice: '⚠️ 守卫当前已禁用,残留的 DSML 闭合标签将直接显示在聊天界面。',
73
87
  statusUnavailable: '配置服务不可用。',
74
88
  saving: '正在保存…',
@@ -77,10 +91,45 @@ window.__ModuleLoader__.load({
77
91
  saveError: '保存失败: {msg}',
78
92
  };
79
93
 
94
+ const ru = {
95
+ title: 'Защита от артефактов DSML',
96
+ sub: 'Фильтрация утекших замыкающих тегов протокола DSML из потоков моделей DeepSeek',
97
+ version: 'Версия',
98
+ updateChecking: 'Проверка обновлений…',
99
+ updateAvailable: 'Доступно обновление: v{v}',
100
+ upToDate: 'Установлена последняя версия',
101
+ updateNow: 'Обновить сейчас',
102
+ updating: 'Обновление…',
103
+ updateSuccess: 'Обновлено до v{v}. Перезапустите службу DSH для применения изменений.',
104
+ updateFailed: 'Ошибка обновления: {msg}',
105
+ checkUpdateFailed: 'Не удалось проверить обновления',
106
+ mode: 'Режим работы',
107
+ modeSanitize: 'Санация (вырезать утекшие замыкающие теги)',
108
+ modeAudit: 'Аудит (только логировать предупреждения в журнал)',
109
+ modeDisabled: 'Отключено (пропускать все потоки без изменений)',
110
+ modelPattern: 'Шаблон модели (RegExp)',
111
+ modelPatternHint: 'Регулярное выражение без учёта регистра для ID модели (по умолчанию: deepseek).',
112
+ providers: 'Целевые провайдеры (опционально)',
113
+ providersPlaceholder: 'все провайдеры (через запятую, напр. commandcode, deepseek-official)',
114
+ providersHint: 'Список ID провайдеров через запятую. Оставьте пустым для защиты всех провайдеров.',
115
+ legacyDetected: 'Обнаружены устаревшие параметры providerId / modelId (действуют как дополнительное правило).',
116
+ legacyClear: 'Очистить устаревшие поля',
117
+ providerId: 'Целевой провайдер (устаревшее)',
118
+ modelId: 'Целевая модель (устаревшее)',
119
+ disabledNotice: '⚠️ Защита отключена. Замыкающие теги DSML будут попадать в интерфейс чата.',
120
+ statusUnavailable: 'Служба настроек недоступна.',
121
+ saving: 'Сохранение…',
122
+ ready: 'Сохранить настройки',
123
+ saved: 'Настройки сохранены',
124
+ saveError: 'Ошибка сохранения: {msg}',
125
+ };
126
+
80
127
  const DEFAULTS = {
81
128
  mode: 'sanitize',
82
- providerId: 'opencode-go',
83
- modelId: 'deepseek-v4-flash',
129
+ modelPattern: 'deepseek',
130
+ providers: '',
131
+ providerId: '',
132
+ modelId: '',
84
133
  };
85
134
 
86
135
  function PluginCard({ ctx: _ctx, t, bare }) {
@@ -115,8 +164,9 @@ window.__ModuleLoader__.load({
115
164
  }
116
165
  const scope = scopeRef.current;
117
166
 
118
- const currentLang = ((typeof _ctx?.locale?.getLocale === 'function' ? _ctx.locale.getLocale() : null) || (typeof navigator !== 'undefined' && navigator.language) || 'en').startsWith('zh') ? 'zh' : 'en';
119
- const dict = currentLang === 'zh' ? zh : en;
167
+ const rawLang = ((typeof _ctx?.locale?.getLocale === 'function' ? _ctx.locale.getLocale() : null) || (typeof navigator !== 'undefined' && navigator.language) || 'en');
168
+ const currentLang = rawLang.startsWith('ru') ? 'ru' : (rawLang.startsWith('zh') ? 'zh' : 'en');
169
+ const dict = currentLang === 'ru' ? ru : (currentLang === 'zh' ? zh : en);
120
170
  const tt = (k, params) => {
121
171
  let str = (typeof t === 'function' ? t(k) : null) || dict[k] || en[k] || k;
122
172
  if (params && typeof params === 'object') {
@@ -137,22 +187,19 @@ window.__ModuleLoader__.load({
137
187
  const syncSnapshot = (snap) => {
138
188
  if (!snap || cancelled) return;
139
189
  const view = snap.view || snap.value || snap;
140
- if (typeof snap === 'object' && 'status' in snap) {
141
- if (snap.status === 'ready' && view) {
142
- setDraft({
143
- mode: view.mode || DEFAULTS.mode,
144
- providerId: view.providerId || DEFAULTS.providerId,
145
- modelId: view.modelId || DEFAULTS.modelId,
146
- });
147
- setStatus('ready');
148
- } else {
149
- setStatus(snap.status);
150
- }
151
- } else if (typeof view === 'object') {
190
+ if (typeof snap === 'object' && 'status' in snap && snap.status !== 'ready') {
191
+ setStatus(snap.status);
192
+ return;
193
+ }
194
+ if (typeof view === 'object' && view !== null) {
152
195
  setDraft({
153
196
  mode: view.mode || DEFAULTS.mode,
154
- providerId: view.providerId || DEFAULTS.providerId,
155
- modelId: view.modelId || DEFAULTS.modelId,
197
+ modelPattern: view.modelPattern !== undefined ? String(view.modelPattern) : DEFAULTS.modelPattern,
198
+ providers: Array.isArray(view.providers)
199
+ ? view.providers.join(', ')
200
+ : (typeof view.providers === 'string' ? view.providers : ''),
201
+ providerId: view.providerId ? String(view.providerId) : '',
202
+ modelId: view.modelId ? String(view.modelId) : '',
156
203
  });
157
204
  setStatus('ready');
158
205
  } else {
@@ -253,9 +300,16 @@ window.__ModuleLoader__.load({
253
300
  setMsg('');
254
301
  try {
255
302
  const failures = [];
256
- for (const key of ['mode', 'providerId', 'modelId']) {
303
+ const entries = [
304
+ ['mode', draft.mode || 'sanitize'],
305
+ ['modelPattern', (draft.modelPattern !== undefined && draft.modelPattern !== null) ? String(draft.modelPattern).trim() : 'deepseek'],
306
+ ['providers', draft.providers ? draft.providers.split(',').map(s => s.trim()).filter(Boolean) : []],
307
+ ['providerId', draft.providerId ? String(draft.providerId).trim() : ''],
308
+ ['modelId', draft.modelId ? String(draft.modelId).trim() : ''],
309
+ ];
310
+ for (const [key, val] of entries) {
257
311
  try {
258
- await scope.set(key, draft[key]);
312
+ await scope.set(key, val);
259
313
  } catch (err) {
260
314
  failures.push(key + ': ' + (err.message || String(err)));
261
315
  }
@@ -452,15 +506,16 @@ window.__ModuleLoader__.load({
452
506
  )
453
507
  ),
454
508
 
455
- // Provider ID Field
509
+ // Model Pattern Field
456
510
  React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
457
511
  React.createElement('label', {
458
512
  style: { fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-label-primary)' },
459
- }, tt('providerId')),
513
+ }, tt('modelPattern')),
460
514
  React.createElement('input', {
461
515
  type: 'text',
462
- value: draft.providerId,
463
- onChange: (e) => setDraft(d => ({ ...d, providerId: e.target.value })),
516
+ value: draft.modelPattern,
517
+ placeholder: 'deepseek',
518
+ onChange: (e) => setDraft(d => ({ ...d, modelPattern: e.target.value })),
464
519
  style: {
465
520
  height: 34,
466
521
  borderRadius: 6,
@@ -470,18 +525,22 @@ window.__ModuleLoader__.load({
470
525
  padding: '0 10px',
471
526
  fontSize: 13,
472
527
  },
473
- })
528
+ }),
529
+ React.createElement('span', {
530
+ style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)', lineHeight: '1.4' },
531
+ }, tt('modelPatternHint'))
474
532
  ),
475
533
 
476
- // Model ID Field
534
+ // Target Providers Field
477
535
  React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
478
536
  React.createElement('label', {
479
537
  style: { fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-label-primary)' },
480
- }, tt('modelId')),
538
+ }, tt('providers')),
481
539
  React.createElement('input', {
482
540
  type: 'text',
483
- value: draft.modelId,
484
- onChange: (e) => setDraft(d => ({ ...d, modelId: e.target.value })),
541
+ value: draft.providers,
542
+ placeholder: tt('providersPlaceholder'),
543
+ onChange: (e) => setDraft(d => ({ ...d, providers: e.target.value })),
485
544
  style: {
486
545
  height: 34,
487
546
  borderRadius: 6,
@@ -491,7 +550,49 @@ window.__ModuleLoader__.load({
491
550
  padding: '0 10px',
492
551
  fontSize: 13,
493
552
  },
494
- })
553
+ }),
554
+ React.createElement('span', {
555
+ style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)', lineHeight: '1.4' },
556
+ }, tt('providersHint'))
557
+ ),
558
+
559
+ // Legacy Provider/Model notice if detected
560
+ (draft.providerId || draft.modelId) && React.createElement('div', {
561
+ style: {
562
+ display: 'flex',
563
+ alignItems: 'flex-start',
564
+ justifyContent: 'space-between',
565
+ gap: 8,
566
+ padding: '8px 10px',
567
+ borderRadius: 6,
568
+ background: 'color-mix(in srgb, var(--dsw-alias-state-warning-primary) 10%, transparent)',
569
+ border: '1px solid color-mix(in srgb, var(--dsw-alias-state-warning-primary) 25%, transparent)',
570
+ fontSize: 12,
571
+ color: 'var(--dsw-alias-label-secondary)',
572
+ },
573
+ },
574
+ React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 2 } },
575
+ React.createElement('span', null, tt('legacyDetected')),
576
+ React.createElement('span', { style: { fontFamily: 'monospace', fontSize: 11 } },
577
+ (draft.providerId ? 'provider: ' + draft.providerId : '') +
578
+ (draft.providerId && draft.modelId ? ' | ' : '') +
579
+ (draft.modelId ? 'model: ' + draft.modelId : '')
580
+ )
581
+ ),
582
+ React.createElement('button', {
583
+ type: 'button',
584
+ onClick: () => setDraft(d => ({ ...d, providerId: '', modelId: '' })),
585
+ style: {
586
+ background: 'transparent',
587
+ border: 'none',
588
+ color: 'var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary))',
589
+ fontSize: 11,
590
+ cursor: 'pointer',
591
+ textDecoration: 'underline',
592
+ whiteSpace: 'nowrap',
593
+ padding: '2px 4px',
594
+ },
595
+ }, tt('legacyClear'))
495
596
  ),
496
597
 
497
598
  // Actions
@@ -574,7 +675,7 @@ window.__ModuleLoader__.load({
574
675
  if (typeof ctx.locale?.register === 'function') {
575
676
  const registerLocale = () => {
576
677
  try {
577
- return ctx.locale.register(NS, { en, zh });
678
+ return ctx.locale.register(NS, { en, zh, ru });
578
679
  } catch (err) {
579
680
  /* Locale namespace already registered or unavailable */
580
681
  }
package/lib/index.js CHANGED
@@ -11,8 +11,10 @@ export const inject = ['llm', 'settings', 'webServer'];
11
11
 
12
12
  const rawConfig = Schema.object({
13
13
  mode: Schema.string().default('sanitize'),
14
- providerId: Schema.string().default('opencode-go'),
15
- modelId: Schema.string().default('deepseek-v4-flash'),
14
+ modelPattern: Schema.string().default('deepseek'),
15
+ providers: Schema.array(Schema.string()).default([]),
16
+ providerId: Schema.string(),
17
+ modelId: Schema.string(),
16
18
  });
17
19
 
18
20
  export const Config = typeof rawConfig.volatile === 'function'
@@ -20,9 +22,37 @@ export const Config = typeof rawConfig.volatile === 'function'
20
22
  : rawConfig.extra('volatile', true);
21
23
 
22
24
  export function matchesScope(o, c) {
23
- const provider = o?.providerId ?? o?.provider?.id ?? o?.provider;
24
- const model = o?.modelId ?? o?.model?.id ?? o?.model?.name ?? o?.model;
25
- return provider == c.providerId && model == c.modelId;
25
+ if (!c || c.mode === 'disabled') return false;
26
+ const provider = String(o?.providerId ?? o?.provider?.id ?? o?.provider ?? '').trim();
27
+ const model = String(o?.modelId ?? o?.model?.id ?? o?.model?.name ?? o?.model ?? '').trim();
28
+ if (!model && !provider) return false;
29
+
30
+ // 1. Backward compatibility: if legacy exact providerId/modelId were specified and match
31
+ if (c.providerId && c.modelId && provider === c.providerId && model === c.modelId) {
32
+ return true;
33
+ }
34
+
35
+ // 2. Provider check: if c.providers is non-empty, provider must be included
36
+ const providersList = Array.isArray(c.providers)
37
+ ? c.providers.map(p => String(p).trim()).filter(Boolean)
38
+ : (typeof c.providers === 'string' && c.providers.trim() ? c.providers.split(',').map(p => p.trim()).filter(Boolean) : []);
39
+
40
+ if (providersList.length > 0) {
41
+ if (!providersList.includes(provider)) return false;
42
+ }
43
+
44
+ // 3. Model pattern check (case-insensitive regex)
45
+ const pattern = (c.modelPattern !== undefined && c.modelPattern !== null && String(c.modelPattern).trim() !== '')
46
+ ? String(c.modelPattern).trim()
47
+ : 'deepseek';
48
+
49
+ try {
50
+ const rx = new RegExp(pattern, 'i');
51
+ return rx.test(model);
52
+ } catch (err) {
53
+ /* Fallback to substring matching if regex syntax is invalid */
54
+ return model.toLowerCase().includes(pattern.toLowerCase());
55
+ }
26
56
  }
27
57
 
28
58
  export async function* sanitizeDsmlArtifacts(s, { mode = 'sanitize', onArtifact } = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-dsml-artifact-guard",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Sanitizes leaked DeepSeek DSML closing tags from model text streams",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",