@goodandready/dsh-dsml-artifact-guard 0.1.5 → 0.2.1

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 CHANGED
@@ -17,8 +17,8 @@
17
17
 
18
18
  <p align="center">
19
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>
20
+ <a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
21
+ <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
22
22
  </p>
23
23
 
24
24
  <table align="center">
@@ -161,7 +161,6 @@ npm run check
161
161
 
162
162
  MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
163
163
 
164
-
165
164
  ## Changed in v0.1.3
166
165
 
167
166
  #8: wrap `llm/stream` in `ctx.effect` so unload unsubscribes.
@@ -176,3 +175,17 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
176
175
  ## Changed in v0.1.5
177
176
 
178
177
  #15: fix Mermaid diagram edge label syntax on GitHub and add mandatory repository support block.
178
+
179
+ ## Changed in v0.2.0
180
+
181
+ - #17: Native DSH Settings Card in slot settings.plugin.item with snapshot status checking, form inputs, and disabled warning indicator.
182
+ - #18: One-click plugin updater with endpoint /api/dsh-dsml-artifact-guard/update, semver comparison, and loopback/same-origin protection.
183
+ - #19: Sanitized repository from internal AGENTS.md and updated .gitignore.
184
+ - #20: Optimized npm package allowlist, eliminating duplicate READMEs and reducing package size by ~40%.
185
+ - #21: Removed legacy build archives from source tree.
186
+
187
+ ## Changed in v0.2.1
188
+
189
+ - #26: Theme tokenization for settings card in `lib/client.js`: eliminated all 42 hardcoded hex/rgba color literals, migrating to canonical `--dsw-alias-...` CSS custom properties and `color-mix()` for flawless Dark and Light mode rendering.
190
+ - #26: Added automated theme regression test guard (`test/theme.test.js`).
191
+ - #26: Fixed `.gitignore` rule for `AGENTS.md` and untracked `package-lock.json` for pure zero-dependency repository hygiene.
@@ -16,7 +16,7 @@
16
16
  </p>
17
17
 
18
18
  <p align="center">
19
- <a href="../README.md"><b>🇬🇧 English</b></a> •
19
+ <a href="README.md"><b>🇬🇧 English</b></a> •
20
20
  <a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
21
21
  <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
22
22
  </p>
@@ -161,7 +161,6 @@ npm run check
161
161
 
162
162
  MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
163
163
 
164
-
165
164
  ## Changed in v0.1.3
166
165
 
167
166
  #8: wrap `llm/stream` in `ctx.effect` so unload unsubscribes.
@@ -176,3 +175,17 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
176
175
  ## Изменения в v0.1.5
177
176
 
178
177
  #15: исправлен синтаксис стрелок диаграммы Mermaid на GitHub и добавлен обязательный блок поддержки репозитория.
178
+
179
+ ## Изменения в v0.2.0
180
+
181
+ - #17: Нативная карточка настроек в слоте settings.plugin.item со статусом снимка, формой и предупреждением об отключении.
182
+ - #18: Модуль автообновления в 1 клик с эндпоинтом /api/dsh-dsml-artifact-guard/update, сравнением версий и защитой same-origin/loopback.
183
+ - #19: Санитайзинг репозитория от внутреннего AGENTS.md и обновление .gitignore.
184
+ - #20: Оптимизация состава npm-пакета, удаление дубликатов README и сокращение размера пакета на ~40%.
185
+ - #21: Очистка дерева исходников от архивных пакетов .tgz.
186
+
187
+ ## Изменения в v0.2.1
188
+
189
+ - #26: Полная токенизация темы для карточки настроек в `lib/client.js`: устранены все 42 вхождения захардкоженных hex/rgba цветов с переходом на канонические переменные ядра `--dsw-alias-...` и `color-mix()` для идеальной читаемости в тёмной и светлой темах.
190
+ - #26: Добавлен автоматический тест-страж соответствия теме (`test/theme.test.js`).
191
+ - #26: Исправлено правило игнорирования `AGENTS.md` в `.gitignore` и снято отслеживание с `package-lock.json` в рамках стандарта нулевых зависимостей.
@@ -16,7 +16,7 @@
16
16
  </p>
17
17
 
18
18
  <p align="center">
19
- <a href="../README.md"><b>🇬🇧 English</b></a> •
19
+ <a href="README.md"><b>🇬🇧 English</b></a> •
20
20
  <a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
21
21
  <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
22
22
  </p>
@@ -161,7 +161,6 @@ npm run check
161
161
 
162
162
  MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
163
163
 
164
-
165
164
  ## Changed in v0.1.3
166
165
 
167
166
  #8: wrap `llm/stream` in `ctx.effect` so unload unsubscribes.
@@ -176,3 +175,17 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
176
175
  ## v0.1.5 变更说明
177
176
 
178
177
  #15: 修复 GitHub 上 Mermaid 流程图边标签语法解析错误,并补全标准项目支持引导区。
178
+
179
+ ## v0.2.0 更新日志
180
+
181
+ - #17: 在 settings.plugin.item 插槽中注册原生设置卡片,支持快照状态检查与禁用警示横幅。
182
+ - #18: 一键在线更新模块,提供 /api/dsh-dsml-artifact-guard/update 端点与同源/回环安全校验。
183
+ - #19: 清理公开代码仓库中的内部 AGENTS.md 规则文件并更新 .gitignore。
184
+ - #20: 优化 npm 发布包白名单,移除冗余 README 副本,安装包体积缩减约 40%FD。
185
+ - #21: 清理源代码目录中的旧版本 .tgz 打包归档。
186
+
187
+ ## v0.2.1 更新日志
188
+
189
+ - #26:配置面板全量主题语义化规范(`lib/client.js`):彻底清除全部 42 处硬编码 hex/rgba 颜色,全面接入 `--dsw-alias-...` 核心变量与 `color-mix()`,保障深色与浅色主题下的极致对比度与可读性。
190
+ - #26:新增前端主题规范自动化回归防护测试(`test/theme.test.js`)。
191
+ - #26:修复 `.gitignore` 对 `AGENTS.md` 的忽略匹配,移除 `package-lock.json` 跟踪以践行极致零依赖标准。
package/lib/client.js ADDED
@@ -0,0 +1,562 @@
1
+ window.__ModuleLoader__.load({
2
+ id: '@goodandready/dsh-dsml-artifact-guard',
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
7
+ const React = require('react');
8
+ const NS = '@goodandready/dsh-dsml-artifact-guard';
9
+
10
+ // Query core chevron primitive when available; fallback SVG otherwise
11
+ let CoreChevron = null;
12
+ try {
13
+ const primitives = require('@deepseek-ai/dsh-client-ui-primitives');
14
+ CoreChevron = primitives && primitives.IconChevronDownOutline14;
15
+ } catch (e) {
16
+ /* Optional core chevron primitive not available; SVG fallback used */
17
+ }
18
+
19
+ const CHEVRON_DOWN_SVG = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"></polyline></svg>';
20
+ const CHEVRON_UP_SVG = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="18 15 12 9 6 15"></polyline></svg>';
21
+
22
+ const en = {
23
+ title: 'DSML Artifact Guard',
24
+ sub: 'Sanitizes leaked terminal DSML protocol closing tags from model text streams',
25
+ version: 'Version',
26
+ updateChecking: 'Checking updates…',
27
+ updateAvailable: 'Update available: v{v}',
28
+ upToDate: 'Up to date',
29
+ updateNow: 'Update Now',
30
+ updating: 'Updating…',
31
+ updateSuccess: 'Updated to v{v}. Restart DSH service to apply changes.',
32
+ updateFailed: 'Update failed: {msg}',
33
+ checkUpdateFailed: 'Check failed',
34
+ mode: 'Operating Mode',
35
+ modeSanitize: 'Sanitize (strip leaked closing tags)',
36
+ modeAudit: 'Audit (log warnings only, preserve output)',
37
+ modeDisabled: 'Disabled (bypass all streams)',
38
+ providerId: 'Target Provider ID',
39
+ modelId: 'Target Model ID',
40
+ disabledNotice: '⚠️ Guard is currently disabled. DSML closing tags will reach the chat interface.',
41
+ statusUnavailable: 'Settings service unavailable.',
42
+ saving: 'Saving…',
43
+ ready: 'Save Settings',
44
+ saved: 'Settings saved',
45
+ saveError: 'Save failed: {msg}',
46
+ };
47
+
48
+ const zh = {
49
+ title: 'DSML 协议标签守卫',
50
+ sub: '实时侦测并清洗模型文本流中泄漏的 DSML 协议末端闭合标签',
51
+ version: '版本',
52
+ updateChecking: '正在检查更新…',
53
+ updateAvailable: '发现新版本: v{v}',
54
+ upToDate: '已是最新版本',
55
+ updateNow: '立即更新',
56
+ updating: '正在更新…',
57
+ updateSuccess: '已更新至 v{v}。请重启 DSH 服务以生效变更。',
58
+ updateFailed: '更新失败: {msg}',
59
+ checkUpdateFailed: '检查更新失败',
60
+ mode: '运行模式',
61
+ modeSanitize: 'Sanitize 清洗(自动剥除残留闭合标签)',
62
+ modeAudit: 'Audit 审计(仅在日志记录警告,不修改正文)',
63
+ modeDisabled: 'Disabled 禁用(直通所有数据流)',
64
+ providerId: '目标服务商标识 (Provider ID)',
65
+ modelId: '目标模型标识 (Model ID)',
66
+ disabledNotice: '⚠️ 守卫当前已禁用,残留的 DSML 闭合标签将直接显示在聊天界面。',
67
+ statusUnavailable: '配置服务不可用。',
68
+ saving: '正在保存…',
69
+ ready: '保存配置',
70
+ saved: '配置已保存',
71
+ saveError: '保存失败: {msg}',
72
+ };
73
+
74
+ const DEFAULTS = {
75
+ mode: 'sanitize',
76
+ providerId: 'opencode-go',
77
+ modelId: 'deepseek-v4-flash',
78
+ };
79
+
80
+ function PluginCard({ ctx: _ctx, t }) {
81
+ const [expanded, setExpanded] = React.useState(false);
82
+ const [draft, setDraft] = React.useState(DEFAULTS);
83
+ const [status, setStatus] = React.useState('loading');
84
+ const [saving, setSaving] = React.useState(false);
85
+ const [msg, setMsg] = React.useState('');
86
+
87
+ // Updater state
88
+ const [updateState, setUpdateState] = React.useState({
89
+ checking: false,
90
+ updateAvailable: false,
91
+ currentVersion: '0.1.5',
92
+ latestVersion: '',
93
+ updating: false,
94
+ notice: '',
95
+ error: '',
96
+ });
97
+
98
+ const scopeRef = React.useRef(null);
99
+ if (!scopeRef.current && _ctx && _ctx.settingsScope) {
100
+ try {
101
+ scopeRef.current = _ctx.settingsScope.bind({ namespace: NS });
102
+ } catch (e) {
103
+ scopeRef.current = null;
104
+ }
105
+ }
106
+ const scope = scopeRef.current;
107
+
108
+ const currentLang = (_ctx?.locale?.get?.() || navigator.language || 'en').startsWith('zh') ? 'zh' : 'en';
109
+ const dict = currentLang === 'zh' ? zh : en;
110
+ const tt = (k, params) => {
111
+ let str = (typeof t === 'function' ? t(k) : null) || dict[k] || en[k] || k;
112
+ if (params && typeof params === 'object') {
113
+ for (const [pk, pv] of Object.entries(params)) {
114
+ str = str.replace(new RegExp('\\{' + pk + '\\}', 'g'), String(pv));
115
+ }
116
+ }
117
+ return str;
118
+ };
119
+
120
+ // Load settings from snapshot
121
+ React.useEffect(() => {
122
+ if (!scope) {
123
+ setStatus('unavailable');
124
+ return;
125
+ }
126
+ let cancelled = false;
127
+ (async () => {
128
+ try {
129
+ const snap = await scope.get();
130
+ if (cancelled) return;
131
+ if (snap && typeof snap === 'object' && 'status' in snap) {
132
+ if (snap.status === 'ready' && snap.value) {
133
+ setDraft({
134
+ mode: snap.value.mode || DEFAULTS.mode,
135
+ providerId: snap.value.providerId || DEFAULTS.providerId,
136
+ modelId: snap.value.modelId || DEFAULTS.modelId,
137
+ });
138
+ setStatus('ready');
139
+ } else {
140
+ setStatus(snap.status);
141
+ }
142
+ } else if (snap && typeof snap === 'object') {
143
+ setDraft({
144
+ mode: snap.mode || DEFAULTS.mode,
145
+ providerId: snap.providerId || DEFAULTS.providerId,
146
+ modelId: snap.modelId || DEFAULTS.modelId,
147
+ });
148
+ setStatus('ready');
149
+ } else {
150
+ setStatus('ready');
151
+ }
152
+ } catch {
153
+ if (!cancelled) setStatus('unavailable');
154
+ }
155
+ })();
156
+ return () => { cancelled = true; };
157
+ }, [scope]);
158
+
159
+ // Check updater status on mount
160
+ React.useEffect(() => {
161
+ let cancelled = false;
162
+ (async () => {
163
+ try {
164
+ setUpdateState(s => ({ ...s, checking: true, error: '' }));
165
+ const res = await fetch('/api/dsh-dsml-artifact-guard/update');
166
+ if (!res.ok) throw new Error(String(res.status));
167
+ const data = await res.json();
168
+ if (cancelled) return;
169
+ setUpdateState(s => ({
170
+ ...s,
171
+ checking: false,
172
+ currentVersion: data.currentVersion || s.currentVersion,
173
+ latestVersion: data.latestVersion || '',
174
+ updateAvailable: !!data.updateAvailable,
175
+ }));
176
+ } catch {
177
+ if (!cancelled) {
178
+ setUpdateState(s => ({ ...s, checking: false, error: tt('checkUpdateFailed') }));
179
+ }
180
+ }
181
+ })();
182
+ return () => { cancelled = true; };
183
+ }, []);
184
+
185
+ const onTriggerUpdate = async () => {
186
+ if (updateState.updating) return;
187
+ setUpdateState(s => ({ ...s, updating: true, error: '', notice: '' }));
188
+ try {
189
+ const res = await fetch('/api/dsh-dsml-artifact-guard/update', {
190
+ method: 'POST',
191
+ headers: {
192
+ 'content-type': 'application/json',
193
+ 'x-dsh-plugin-update': '1',
194
+ },
195
+ });
196
+ const data = await res.json();
197
+ if (!res.ok || !data.ok && data.error) {
198
+ throw new Error(data.error || String(res.status));
199
+ }
200
+ setUpdateState(s => ({
201
+ ...s,
202
+ updating: false,
203
+ updateAvailable: false,
204
+ currentVersion: data.updatedVersion || s.latestVersion,
205
+ notice: tt('updateSuccess', { v: data.updatedVersion || s.latestVersion }),
206
+ }));
207
+ } catch (err) {
208
+ setUpdateState(s => ({
209
+ ...s,
210
+ updating: false,
211
+ error: tt('updateFailed', { msg: err.message || String(err) }),
212
+ }));
213
+ }
214
+ };
215
+
216
+ const onSave = async () => {
217
+ if (!scope || saving) return;
218
+ setSaving(true);
219
+ setMsg('');
220
+ try {
221
+ const failures = [];
222
+ for (const key of ['mode', 'providerId', 'modelId']) {
223
+ try {
224
+ await scope.set(key, draft[key]);
225
+ } catch (err) {
226
+ failures.push(key + ': ' + (err.message || String(err)));
227
+ }
228
+ }
229
+ if (failures.length > 0) {
230
+ setMsg(tt('saveError', { msg: failures.join(', ') }));
231
+ } else {
232
+ setMsg(tt('saved'));
233
+ setTimeout(() => setMsg(''), 3000);
234
+ }
235
+ } catch (err) {
236
+ setMsg(tt('saveError', { msg: err.message || String(err) }));
237
+ } finally {
238
+ setSaving(false);
239
+ }
240
+ };
241
+
242
+ const isDisabled = draft.mode === 'disabled';
243
+
244
+ return React.createElement('div', {
245
+ 'data-dsh-plugin': NS,
246
+ className: 'dsh-dsml-artifact-guard-card',
247
+ style: {
248
+ borderRadius: 12,
249
+ border: '1px solid var(--dsw-alias-border-l2)',
250
+ background: 'var(--dsw-alias-bg-card, var(--dsw-alias-bg-layer-3))',
251
+ overflow: 'hidden',
252
+ marginBottom: 12,
253
+ },
254
+ },
255
+ // Header
256
+ React.createElement('div', {
257
+ onClick: () => setExpanded(v => !v),
258
+ style: {
259
+ display: 'flex',
260
+ alignItems: 'center',
261
+ justifyContent: 'space-between',
262
+ padding: '14px 16px',
263
+ cursor: 'pointer',
264
+ userSelect: 'none',
265
+ },
266
+ },
267
+ React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
268
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
269
+ React.createElement('span', { style: { fontSize: 16 } }, '🛡️'),
270
+ React.createElement('span', {
271
+ style: {
272
+ fontSize: 15,
273
+ fontWeight: 600,
274
+ color: 'var(--dsw-alias-label-primary)',
275
+ },
276
+ }, tt('title')),
277
+ React.createElement('span', {
278
+ style: {
279
+ fontSize: 11,
280
+ fontWeight: 500,
281
+ padding: '2px 6px',
282
+ borderRadius: 6,
283
+ background: 'color-mix(in srgb, var(--dsw-alias-brand-primary, var(--dsw-alias-state-brand-primary)) 15%, transparent)',
284
+ color: 'var(--dsw-alias-brand-primary, var(--dsw-alias-state-brand-primary))',
285
+ },
286
+ }, 'v' + updateState.currentVersion),
287
+ isDisabled && React.createElement('span', {
288
+ style: {
289
+ fontSize: 11,
290
+ fontWeight: 600,
291
+ padding: '2px 6px',
292
+ borderRadius: 6,
293
+ background: 'color-mix(in srgb, var(--dsw-alias-state-danger-primary, var(--dsw-alias-state-error-primary)) 15%, transparent)',
294
+ color: 'var(--dsw-alias-state-danger-primary, var(--dsw-alias-state-error-primary))',
295
+ },
296
+ }, 'OFF')
297
+ ),
298
+ React.createElement('span', {
299
+ style: {
300
+ fontSize: 13,
301
+ color: 'var(--dsw-alias-label-secondary)',
302
+ },
303
+ }, tt('sub'))
304
+ ),
305
+ React.createElement('div', {
306
+ style: {
307
+ display: 'flex',
308
+ alignItems: 'center',
309
+ color: 'var(--dsw-alias-label-tertiary)',
310
+ },
311
+ },
312
+ CoreChevron
313
+ ? React.createElement(CoreChevron, { style: { transform: expanded ? 'rotate(180deg)' : 'none', transition: 'transform 0.2s' } })
314
+ : React.createElement('span', { dangerouslySetInnerHTML: { __html: expanded ? CHEVRON_UP_SVG : CHEVRON_DOWN_SVG } })
315
+ )
316
+ ),
317
+
318
+ // Body
319
+ expanded && React.createElement('div', {
320
+ style: {
321
+ padding: '0 16px 16px',
322
+ borderTop: '1px solid var(--dsw-alias-border-l2)',
323
+ },
324
+ },
325
+ // One-Click Updater block
326
+ React.createElement('div', {
327
+ style: {
328
+ padding: '12px 14px',
329
+ margin: '14px 0',
330
+ background: 'color-mix(in srgb, var(--dsw-alias-label-primary) 3%, transparent)',
331
+ borderRadius: 8,
332
+ border: '1px solid var(--dsw-alias-border-l2)',
333
+ display: 'flex',
334
+ alignItems: 'center',
335
+ justifyContent: 'space-between',
336
+ flexWrap: 'wrap',
337
+ gap: 8,
338
+ },
339
+ },
340
+ React.createElement('div', { style: { fontSize: 13, display: 'flex', alignItems: 'center', gap: 8 } },
341
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } },
342
+ tt('version') + ': ' + updateState.currentVersion
343
+ ),
344
+ updateState.checking && React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 12 } },
345
+ tt('updateChecking')
346
+ ),
347
+ !updateState.checking && updateState.updateAvailable && React.createElement('span', { style: { color: 'var(--dsw-alias-state-warning-primary)', fontWeight: 600, fontSize: 12 } },
348
+ tt('updateAvailable', { v: updateState.latestVersion })
349
+ ),
350
+ !updateState.checking && !updateState.updateAvailable && !updateState.error && React.createElement('span', { style: { color: 'var(--dsw-alias-state-success-primary)', fontSize: 12 } },
351
+ '✓ ' + tt('upToDate')
352
+ ),
353
+ updateState.error && React.createElement('span', { style: { color: 'var(--dsw-alias-state-danger-primary, var(--dsw-alias-state-error-primary))', fontSize: 12 } },
354
+ updateState.error
355
+ )
356
+ ),
357
+ updateState.updateAvailable && React.createElement('button', {
358
+ onClick: onTriggerUpdate,
359
+ disabled: updateState.updating,
360
+ style: {
361
+ padding: '6px 14px',
362
+ borderRadius: 6,
363
+ background: 'var(--dsw-alias-label-primary)',
364
+ color: 'var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-card))',
365
+ border: 0,
366
+ fontSize: 12,
367
+ fontWeight: 600,
368
+ cursor: updateState.updating ? 'default' : 'pointer',
369
+ },
370
+ }, updateState.updating ? tt('updating') : tt('updateNow'))
371
+ ),
372
+ updateState.notice && React.createElement('div', {
373
+ style: {
374
+ padding: '8px 12px',
375
+ marginBottom: 12,
376
+ background: 'color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent)',
377
+ color: 'var(--dsw-alias-state-success-primary)',
378
+ borderRadius: 6,
379
+ fontSize: 12,
380
+ border: '1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 20%, transparent)',
381
+ },
382
+ }, updateState.notice),
383
+
384
+ // Disabled Warning Banner
385
+ isDisabled && React.createElement('div', {
386
+ style: {
387
+ padding: '8px 12px',
388
+ marginBottom: 14,
389
+ background: 'color-mix(in srgb, var(--dsw-alias-state-danger-primary, var(--dsw-alias-state-error-primary)) 10%, transparent)',
390
+ color: 'var(--dsw-alias-state-danger-primary, var(--dsw-alias-state-error-primary))',
391
+ borderRadius: 6,
392
+ fontSize: 12,
393
+ border: '1px solid color-mix(in srgb, var(--dsw-alias-state-danger-primary, var(--dsw-alias-state-error-primary)) 20%, transparent)',
394
+ },
395
+ }, tt('disabledNotice')),
396
+
397
+ // Status Loading / Unavailable
398
+ status === 'loading' && React.createElement('div', {
399
+ style: { padding: '16px 0', fontSize: 13, color: 'var(--dsw-alias-label-tertiary)' },
400
+ }, tt('saving')),
401
+ status === 'unavailable' && React.createElement('div', {
402
+ style: { padding: '16px 0', fontSize: 13, color: 'var(--dsw-alias-state-danger-primary, var(--dsw-alias-state-error-primary))' },
403
+ }, tt('statusUnavailable')),
404
+
405
+ // Settings Fields
406
+ status === 'ready' && React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 14 } },
407
+ // Mode Select
408
+ React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
409
+ React.createElement('label', {
410
+ style: { fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-label-primary)' },
411
+ }, tt('mode')),
412
+ React.createElement('select', {
413
+ value: draft.mode,
414
+ onChange: (e) => setDraft(d => ({ ...d, mode: e.target.value })),
415
+ style: {
416
+ height: 34,
417
+ borderRadius: 6,
418
+ border: '1px solid var(--dsw-alias-border-l2)',
419
+ background: 'var(--dsw-alias-bg-field, var(--dsw-alias-bg-layer-2))',
420
+ color: 'var(--dsw-alias-label-primary)',
421
+ padding: '0 10px',
422
+ fontSize: 13,
423
+ },
424
+ },
425
+ React.createElement('option', { value: 'sanitize' }, tt('modeSanitize')),
426
+ React.createElement('option', { value: 'audit' }, tt('modeAudit')),
427
+ React.createElement('option', { value: 'disabled' }, tt('modeDisabled'))
428
+ )
429
+ ),
430
+
431
+ // Provider ID Field
432
+ React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
433
+ React.createElement('label', {
434
+ style: { fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-label-primary)' },
435
+ }, tt('providerId')),
436
+ React.createElement('input', {
437
+ type: 'text',
438
+ value: draft.providerId,
439
+ onChange: (e) => setDraft(d => ({ ...d, providerId: e.target.value })),
440
+ style: {
441
+ height: 34,
442
+ borderRadius: 6,
443
+ border: '1px solid var(--dsw-alias-border-l2)',
444
+ background: 'var(--dsw-alias-bg-field, var(--dsw-alias-bg-layer-2))',
445
+ color: 'var(--dsw-alias-label-primary)',
446
+ padding: '0 10px',
447
+ fontSize: 13,
448
+ },
449
+ })
450
+ ),
451
+
452
+ // Model ID Field
453
+ React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
454
+ React.createElement('label', {
455
+ style: { fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-label-primary)' },
456
+ }, tt('modelId')),
457
+ React.createElement('input', {
458
+ type: 'text',
459
+ value: draft.modelId,
460
+ onChange: (e) => setDraft(d => ({ ...d, modelId: e.target.value })),
461
+ style: {
462
+ height: 34,
463
+ borderRadius: 6,
464
+ border: '1px solid var(--dsw-alias-border-l2)',
465
+ background: 'var(--dsw-alias-bg-field, var(--dsw-alias-bg-layer-2))',
466
+ color: 'var(--dsw-alias-label-primary)',
467
+ padding: '0 10px',
468
+ fontSize: 13,
469
+ },
470
+ })
471
+ ),
472
+
473
+ // Actions
474
+ React.createElement('div', {
475
+ style: {
476
+ display: 'flex',
477
+ alignItems: 'center',
478
+ gap: 12,
479
+ paddingTop: 12,
480
+ borderTop: '1px solid var(--dsw-alias-border-l2)',
481
+ marginTop: 4,
482
+ },
483
+ },
484
+ React.createElement('button', {
485
+ onClick: onSave,
486
+ disabled: saving || status !== 'ready',
487
+ style: {
488
+ height: 34,
489
+ padding: '0 18px',
490
+ border: 0,
491
+ borderRadius: 8,
492
+ background: 'var(--dsw-alias-label-primary)',
493
+ color: 'var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-card))',
494
+ fontSize: 13,
495
+ fontWeight: 600,
496
+ cursor: saving ? 'default' : 'pointer',
497
+ },
498
+ }, saving ? tt('saving') : tt('ready')),
499
+ msg && React.createElement('span', {
500
+ style: {
501
+ fontSize: 13,
502
+ color: msg === tt('saved') ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-danger-primary, var(--dsw-alias-state-error-primary))',
503
+ },
504
+ }, msg)
505
+ )
506
+ )
507
+ )
508
+ );
509
+ }
510
+
511
+ exports.inject = ['slots', 'settingsScope', 'locale'];
512
+ exports.apply = function apply(ctx) {
513
+ if (typeof ctx.locale?.register === 'function') {
514
+ try {
515
+ ctx.locale.register(NS, { en, zh });
516
+ } catch (err) {
517
+ /* Locale namespace already registered or unavailable */
518
+ }
519
+ }
520
+
521
+ function registerSlotWhenReady(slotName, registerFn) {
522
+ if (!ctx.slots) return;
523
+ if (typeof ctx.slots.inject === 'function') {
524
+ try {
525
+ ctx.slots.inject(slotName, () => {
526
+ try {
527
+ return registerFn();
528
+ } catch (err) {
529
+ console.warn('[' + NS + '] Error registering slot ' + slotName + ':', err);
530
+ }
531
+ });
532
+ return;
533
+ } catch (err) {
534
+ console.warn('[' + NS + '] Failed to inject slot ' + slotName + ':', err);
535
+ }
536
+ }
537
+ if (typeof ctx.slots.register === 'function') {
538
+ try {
539
+ registerFn();
540
+ } catch (err) {
541
+ console.warn('[' + NS + '] Failed direct registration for ' + slotName + ':', err);
542
+ }
543
+ }
544
+ }
545
+
546
+ registerSlotWhenReady('settings.plugin.item', () =>
547
+ ctx.slots.register(
548
+ {
549
+ name: 'settings.plugin.item',
550
+ key: NS,
551
+ order: 95,
552
+ locale: NS,
553
+ inject: () => ({ ctx }),
554
+ },
555
+ (props) => React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx })
556
+ )
557
+ );
558
+ };
559
+
560
+ return module.exports;
561
+ },
562
+ });
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import Schema from '@deepseek-ai/schemastery';
2
+ import { registerPluginUpdater } from './updater.js';
2
3
 
3
4
  const TAIL = /(?:<\/|DSML|parameter>\s*)?(?:<\/|DSML|invoke>\s*)?<\/|DSML|tool_calls>\s*$/u;
4
5
  const OPEN_TAG = /<|DSML|(?:tool_calls|invoke|parameter)[^>]*>/gu;
@@ -7,7 +8,7 @@ const KEEP = 96;
7
8
  const SCAN_BUFFER_SAFE = 64;
8
9
 
9
10
  export const name = '@goodandready/dsh-dsml-artifact-guard';
10
- export const inject = ['llm'];
11
+ export const inject = ['llm', 'settings', 'webServer'];
11
12
 
12
13
  export const Config = Schema.object({
13
14
  mode: Schema.string().default('sanitize'),
@@ -16,8 +17,9 @@ export const Config = Schema.object({
16
17
  });
17
18
 
18
19
  export function matchesScope(o, c) {
19
- return (o?.providerId ?? o?.provider?.id ?? o?.provider) == c.providerId
20
- && (o?.modelId ?? o?.model?.id ?? o?.model?.name ?? o?.model) == c.modelId;
20
+ const provider = o?.providerId ?? o?.provider?.id ?? o?.provider;
21
+ const model = o?.modelId ?? o?.model?.id ?? o?.model?.name ?? o?.model;
22
+ return provider == c.providerId && model == c.modelId;
21
23
  }
22
24
 
23
25
  export async function* sanitizeDsmlArtifacts(s, { mode = 'sanitize', onArtifact } = {}) {
@@ -90,16 +92,57 @@ export async function* sanitizeDsmlArtifacts(s, { mode = 'sanitize', onArtifact
90
92
  yield* flush(true);
91
93
  }
92
94
 
93
- export function apply(ctx, c = {}) {
94
- const e = Config(c) ?? {};
95
- // #8: dispose with ctx.effect so unload unsubscribes llm/stream
95
+ export function apply(ctx, initialConfig = {}) {
96
+ const NS = '@goodandready/dsh-dsml-artifact-guard';
97
+ let activeConfig = Config(initialConfig) ?? {};
98
+
99
+ let getConfig = () => activeConfig;
100
+
101
+ // Register settings scope inside ctx.effect
102
+ if (typeof ctx.inject === 'function') {
103
+ ctx.inject(['settings'], (sctx) => {
104
+ try {
105
+ const scope = sctx.settings.register(NS, Config, { base: initialConfig });
106
+ getConfig = () => Config(scope?.get() ?? initialConfig) ?? {};
107
+ if (typeof scope?.watch === 'function') {
108
+ sctx.effect(() => scope.watch(() => {
109
+ activeConfig = getConfig();
110
+ }));
111
+ }
112
+ } catch (err) {
113
+ ctx.logger?.warn?.(`[dsh-dsml-artifact-guard] settings registration skipped: ${String(err)}`);
114
+ }
115
+ });
116
+ }
117
+
118
+ // Register one-click updater endpoint
119
+ if (ctx.webServer && typeof ctx.webServer.register === 'function') {
120
+ const mountUpdater = () => registerPluginUpdater(ctx, {
121
+ endpoint: '/api/dsh-dsml-artifact-guard/update',
122
+ packageName: '@goodandready/dsh-dsml-artifact-guard',
123
+ manifestUrl: new URL('../package.json', import.meta.url),
124
+ });
125
+ if (typeof ctx.effect === 'function') {
126
+ ctx.effect(mountUpdater, 'dsh-dsml-artifact-guard: plugin updater');
127
+ } else {
128
+ mountUpdater();
129
+ }
130
+ }
131
+
132
+ // Hook into llm/stream with ctx.effect lifecycle cleanup
96
133
  ctx.effect(() => ctx.on('llm/stream', (o, next) => {
134
+ const current = getConfig();
97
135
  const s = next(o);
98
- return e.mode === 'disabled' || !matchesScope(o, e)
136
+ return current.mode === 'disabled' || !matchesScope(o, current)
99
137
  ? s
100
138
  : sanitizeDsmlArtifacts(s, {
101
- mode: e.mode,
139
+ mode: current.mode,
102
140
  onArtifact: (x) => ctx.logger?.info?.('DSML closing-tag artifact detected', x),
103
141
  });
104
142
  }));
105
143
  }
144
+
145
+ // Preflight helper: verify request method
146
+ export function checkRequestMethod(req, expected = 'POST') {
147
+ return req && req.method === expected;
148
+ }
package/lib/updater.js ADDED
@@ -0,0 +1,267 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { basename, dirname, isAbsolute, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const UPDATE_HEADER = 'x-dsh-plugin-update';
9
+ const UPDATE_TIMEOUT_MS = 10 * 60000;
10
+ const VERSION_CACHE_MS = 5 * 60000;
11
+ let latestCache;
12
+
13
+ function header(request, name) {
14
+ const value = request?.headers?.[name];
15
+ return Array.isArray(value) ? value[0] : value;
16
+ }
17
+
18
+ export function isLoopback(value) {
19
+ const address = value?.toLowerCase().replace(/^\[|\]$/g, '');
20
+ return address === 'localhost' || address === 'localhost.' || address === '::1'
21
+ || address?.startsWith('127.') === true
22
+ || address?.startsWith('::ffff:127.') === true;
23
+ }
24
+
25
+ export function isTrustedUpdateRequest(request) {
26
+ if (header(request, UPDATE_HEADER) !== '1') return false;
27
+ if (!isLoopback(request.socket?.remoteAddress)) return false;
28
+ const site = header(request, 'sec-fetch-site');
29
+ if (site !== undefined && site !== 'same-origin') return false;
30
+ const origin = header(request, 'origin');
31
+ const host = header(request, 'host');
32
+ if (origin === undefined || host === undefined) return false;
33
+ try {
34
+ const url = new URL(origin);
35
+ return (url.protocol === 'http:' || url.protocol === 'https:')
36
+ && isLoopback(url.hostname) && url.host === host;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ function validProfileName(value) {
43
+ return typeof value === 'string' && value !== '' && value !== '.' && value !== '..'
44
+ && !value.includes('/') && !value.includes('\\') && !/[\0-\x1f\x7f]/.test(value);
45
+ }
46
+
47
+ function profileNameFromArgv(argv) {
48
+ for (let index = 2; index < argv.length; index += 1) {
49
+ if (argv[index] === '--profile') return argv[index + 1];
50
+ if (argv[index]?.startsWith('--profile=')) return argv[index].slice('--profile='.length);
51
+ }
52
+ return argv[2] === 'web' ? 'web' : undefined;
53
+ }
54
+
55
+ function findDshCliEntry() {
56
+ const value = process.argv[1];
57
+ if (value === undefined || value === '') return undefined;
58
+ const entry = value.startsWith('file:') ? fileURLToPath(value) : resolve(process.cwd(), value);
59
+ if (!existsSync(entry)) return undefined;
60
+ for (let directory = dirname(entry); ; directory = dirname(directory)) {
61
+ const manifestPath = resolve(directory, 'package.json');
62
+ if (existsSync(manifestPath)) {
63
+ try {
64
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
65
+ const bin = typeof manifest.bin === 'string'
66
+ ? manifest.bin
67
+ : typeof manifest.bin === 'object' && manifest.bin !== null
68
+ ? manifest.bin.dsh
69
+ : undefined;
70
+ if (manifest.name === '@deepseek-ai/dsh' && typeof bin === 'string'
71
+ && !isAbsolute(bin) && resolve(directory, bin) === resolve(entry)) return entry;
72
+ } catch {
73
+ // Continue searching parent directories
74
+ }
75
+ }
76
+ const parent = dirname(directory);
77
+ if (parent === directory) return undefined;
78
+ }
79
+ }
80
+
81
+ export function runtime() {
82
+ const profileDir = resolve(process.env.DSH_PROFILE_DIR
83
+ ?? resolve(homedir(), '.dsh', 'profiles', 'web'));
84
+ const selected = profileNameFromArgv(process.argv);
85
+ const profileName = validProfileName(selected)
86
+ ? selected
87
+ : validProfileName(basename(profileDir)) ? basename(profileDir) : 'web';
88
+ const cliEntry = findDshCliEntry();
89
+ return cliEntry === undefined ? { profileName, profileDir } : { profileName, profileDir, cliEntry };
90
+ }
91
+
92
+ export function parseSemver(value) {
93
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(String(value || '').trim());
94
+ if (match === null) return undefined;
95
+ return {
96
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
97
+ prerelease: match[4]?.split('.') ?? [],
98
+ };
99
+ }
100
+
101
+ function comparePrerelease(left, right) {
102
+ if (left.length === 0 || right.length === 0) return left.length === right.length ? 0 : left.length === 0 ? 1 : -1;
103
+ const length = Math.max(left.length, right.length);
104
+ for (let index = 0; index < length; index += 1) {
105
+ const a = left[index];
106
+ const b = right[index];
107
+ if (a === undefined || b === undefined) return a === b ? 0 : a === undefined ? -1 : 1;
108
+ if (a === b) continue;
109
+ const aNumeric = /^\d+$/.test(a);
110
+ const bNumeric = /^\d+$/.test(b);
111
+ if (aNumeric && bNumeric) {
112
+ const aNumber = BigInt(a);
113
+ const bNumber = BigInt(b);
114
+ if (aNumber !== bNumber) return aNumber > bNumber ? 1 : -1;
115
+ continue;
116
+ }
117
+ if (aNumeric !== bNumeric) return aNumeric ? -1 : 1;
118
+ return a > b ? 1 : -1;
119
+ }
120
+ return 0;
121
+ }
122
+
123
+ export function isNewerVersion(currentValue, candidateValue) {
124
+ const current = parseSemver(currentValue);
125
+ const candidate = parseSemver(candidateValue);
126
+ if (current === undefined || candidate === undefined) return false;
127
+ for (let index = 0; index < 3; index += 1) {
128
+ if (candidate.core[index] !== current.core[index]) return candidate.core[index] > current.core[index];
129
+ }
130
+ return comparePrerelease(candidate.prerelease, current.prerelease) > 0;
131
+ }
132
+
133
+ export async function latestVersion(packageName, registry = 'https://registry.npmjs.org') {
134
+ if (latestCache?.packageName === packageName && latestCache.registry === registry && Date.now() < latestCache.expiresAt) {
135
+ return latestCache.version;
136
+ }
137
+ try {
138
+ const cleanReg = registry.replace(/\/$/, '');
139
+ const response = await fetch(`${cleanReg}/${encodeURIComponent(packageName)}/latest`, {
140
+ signal: AbortSignal.timeout(8000),
141
+ headers: { accept: 'application/json' },
142
+ });
143
+ if (!response.ok) return undefined;
144
+ const value = await response.json();
145
+ if (typeof value.version !== 'string' || value.version === '') return undefined;
146
+ latestCache = { packageName, registry, version: value.version, expiresAt: Date.now() + VERSION_CACHE_MS };
147
+ return value.version;
148
+ } catch {
149
+ return undefined;
150
+ }
151
+ }
152
+
153
+ export async function currentVersion(manifestUrl) {
154
+ const raw = await readFile(manifestUrl, 'utf8');
155
+ const value = JSON.parse(raw);
156
+ if (typeof value.version !== 'string' || value.version === '') throw new Error('Cannot read current plugin version.');
157
+ return value.version;
158
+ }
159
+
160
+ export async function getUpdateStatus(options, target = runtime()) {
161
+ const current = await currentVersion(options.manifestUrl);
162
+ const latest = await latestVersion(options.packageName, options.registry ?? 'https://registry.npmjs.org');
163
+ return {
164
+ packageName: options.packageName,
165
+ currentVersion: current,
166
+ ...(latest === undefined ? {} : { latestVersion: latest }),
167
+ latestCheckFailed: latest === undefined,
168
+ updateAvailable: latest !== undefined && isNewerVersion(current, latest),
169
+ profileName: target.profileName,
170
+ canAutoUpdate: target.cliEntry !== undefined,
171
+ };
172
+ }
173
+
174
+ export async function installExact(target, packageSpec, options = {}) {
175
+ if (target.cliEntry === undefined) throw new Error('Automatic update is unavailable in this runtime.');
176
+ return new Promise((resolvePromise, reject) => {
177
+ const args = [
178
+ target.cliEntry, 'plugin', '--profile', target.profileName, 'add',
179
+ packageSpec,
180
+ `--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
181
+ ];
182
+ const child = spawn(process.execPath, args, {
183
+ cwd: target.profileDir,
184
+ windowsHide: true,
185
+ stdio: ['ignore', 'pipe', 'pipe'],
186
+ env: { ...process.env, NO_COLOR: '1' },
187
+ });
188
+ let detail = '';
189
+ child.stdout?.on('data', chunk => { detail = (detail + String(chunk)).slice(-4000); });
190
+ child.stderr?.on('data', chunk => { detail = (detail + String(chunk)).slice(-4000); });
191
+ const timer = setTimeout(() => {
192
+ child.kill();
193
+ reject(new Error('Update timed out; use the normal DSH update flow.'));
194
+ }, UPDATE_TIMEOUT_MS);
195
+ child.once('error', error => { clearTimeout(timer); reject(error); });
196
+ child.once('exit', code => {
197
+ clearTimeout(timer);
198
+ if (code === 0) resolvePromise();
199
+ else reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`));
200
+ });
201
+ });
202
+ }
203
+
204
+ export function registerPluginUpdater(ctx, options) {
205
+ let installing = false;
206
+ return ctx.webServer.register({
207
+ kind: 'exact',
208
+ path: options.endpoint || '/api/dsh-dsml-artifact-guard/update',
209
+ handler: async (req, res) => {
210
+ try {
211
+ const target = runtime();
212
+ if (req.method === 'GET' || req.method === 'HEAD') {
213
+ const payload = await getUpdateStatus(options, target);
214
+ res.writeHead(200, {
215
+ 'content-type': 'application/json; charset=utf-8',
216
+ 'cache-control': 'no-store',
217
+ });
218
+ res.end(req.method === 'HEAD' ? undefined : JSON.stringify(payload));
219
+ return;
220
+ }
221
+ if (req.method !== 'POST') {
222
+ res.writeHead(405, { allow: 'GET, HEAD, POST' });
223
+ res.end();
224
+ return;
225
+ }
226
+ if (!isTrustedUpdateRequest(req)) {
227
+ res.writeHead(403, { 'content-type': 'application/json; charset=utf-8' });
228
+ res.end(JSON.stringify({ ok: false, error: 'Untrusted or cross-origin request rejected.' }));
229
+ return;
230
+ }
231
+ if (installing) {
232
+ res.writeHead(409, { 'content-type': 'application/json; charset=utf-8' });
233
+ res.end(JSON.stringify({ ok: false, error: 'This plugin is already updating.' }));
234
+ return;
235
+ }
236
+ installing = true;
237
+ try {
238
+ const before = await getUpdateStatus(options, target);
239
+ if (before.latestVersion === undefined) {
240
+ res.writeHead(503, { 'content-type': 'application/json; charset=utf-8' });
241
+ res.end(JSON.stringify({ ok: false, error: 'The latest version is temporarily unavailable.' }));
242
+ return;
243
+ }
244
+ if (!before.updateAvailable) {
245
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
246
+ res.end(JSON.stringify({ ...before, updated: false, message: 'Already up to date.' }));
247
+ return;
248
+ }
249
+ await installExact(target, `${options.packageName}@${before.latestVersion}`, options);
250
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
251
+ res.end(JSON.stringify({
252
+ ...before,
253
+ updated: true,
254
+ updatedVersion: before.latestVersion,
255
+ restartRequired: true,
256
+ }));
257
+ } finally {
258
+ installing = false;
259
+ }
260
+ } catch (error) {
261
+ (ctx.logger || console).warn?.(`[dsh-dsml-artifact-guard] plugin updater failed: ${String(error)}`);
262
+ res.writeHead(503, { 'content-type': 'application/json; charset=utf-8' });
263
+ res.end(JSON.stringify({ ok: false, error: 'Plugin update failed; see server logs.' }));
264
+ }
265
+ },
266
+ });
267
+ }
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-dsml-artifact-guard",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "description": "Sanitizes leaked DeepSeek DSML closing tags from model text streams",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "license": "MIT",
8
8
  "files": [
9
- "lib",
9
+ "lib/*.js",
10
10
  "cordis.patch.yml",
11
11
  "README.md",
12
- "docs",
12
+ "README.ru.md",
13
+ "README.zh.md",
13
14
  "LICENSE"
14
15
  ],
15
16
  "scripts": {
@@ -19,10 +20,24 @@
19
20
  "dsh": {
20
21
  "bundle": {
21
22
  "patch": "./cordis.patch.yml"
23
+ },
24
+ "client": {
25
+ "platform": "web",
26
+ "inject": [
27
+ "@deepseek-ai/dsh-client-locale",
28
+ "@deepseek-ai/dsh-client-ui-slots",
29
+ "@deepseek-ai/dsh-client-ui-settings"
30
+ ]
22
31
  }
23
32
  },
24
33
  "peerDependencies": {
25
34
  "@deepseek-ai/cordis": "^4.0.1",
26
35
  "@deepseek-ai/schemastery": "^3.18.1"
36
+ },
37
+ "exports": {
38
+ ".": "./lib/index.js",
39
+ "./client": "./lib/client.js",
40
+ "./cordis.patch.yml": "./cordis.patch.yml",
41
+ "./package.json": "./package.json"
27
42
  }
28
43
  }
@@ -1,36 +0,0 @@
1
- # Design Contract: dsh-dsml-artifact-guard
2
-
3
- ## 1. Product Purpose & User Scenarios
4
- - **Purpose**: Host-only streaming guard for DeepSeek Harness that intercepts assistant stream chunks and strips leaked DSML closing tags (`</|DSML|parameter>`, `</|DSML|invoke>`, `</|DSML|tool_calls>`) emitted by upstream providers before text reaches the user interface.
5
- - **Primary Scenarios**:
6
- - Assistant responds through an upstream gateway exhibiting DSML leakage (e.g. `opencode-go` + `deepseek-v4-flash`). Leaked protocol closing tags at the end of the response are stripped automatically.
7
- - User and assistant discuss DSML syntax or generate markdown/code examples with DSML tags. Legitimate open tags and their matching closing tags are preserved intact (fail-open).
8
- - Out-of-scope providers/models pass through with zero buffering or regex evaluation.
9
-
10
- ## 2. User Surfaces
11
- - **Web UI**: UI отсутствует на текущем этапе (host-only middleware plugin).
12
- - **DSH Configuration**:
13
- - `mode`: `"sanitize"` (default, strips tags), `"audit"` (logs to `ctx.logger.info`), `"disabled"` (bypasses).
14
- - `providerId`: target provider (default `"opencode-go"`).
15
- - `modelId`: target model (default `"deepseek-v4-flash"`).
16
- - **Documentation**:
17
- - Full trilingual documentation: `README.md` (EN), `docs/README.ru.md` (RU), `docs/README.zh.md` (ZH).
18
-
19
- ## 3. Streaming & Buffering Contract
20
- - **Synchronous Hook Contract**: The `llm/stream` listener returns an `AsyncIterable` synchronously. Returning a `Promise` breaks the Cordis event dispatcher.
21
- - **Sliding Buffer (KEEP = 96)**: Preserves the terminal window across chunk splits.
22
- - **Tag Balancing**: Tracks opening and closing DSML tags across the stream so that only orphaned closing tags at the terminal tail are sanitized, leaving balanced code blocks untouched.
23
-
24
- ## 4. Do / Don't
25
- - **Do**:
26
- - Preserve fail-open guarantees for all user prose.
27
- - Dispose hook registration with `ctx.effect`.
28
- - Pass non-target provider/model streams directly through.
29
- - **Don't**:
30
- - Do not make the stream hook async.
31
- - Do not drop legitimate DSML discussion in chat.
32
- - Do not add heavy client-side UI for simple host middleware.
33
-
34
- ## 5. Decisions Log
35
- - **2026-09-06**: Added `ctx.effect` lifecycle wrapper and switched patch to schema-driven defaults (`#8, #9`).
36
- - **2026-09-12**: Changed schema default mode to `sanitize` for out-of-the-box protection, implemented robust open/close tag balancing across streaming chunks, and tracked `package-lock.json` (`#11`).