@goodandready/dsh-dsml-artifact-guard 0.1.4 → 0.2.0
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 +28 -7
- package/{docs/README.ru.md → README.ru.md} +27 -6
- package/{docs/README.zh.md → README.zh.md} +26 -5
- package/lib/client.js +562 -0
- package/lib/index.js +51 -8
- package/lib/updater.js +267 -0
- package/package.json +18 -3
- package/docs/design/DESIGN.md +0 -36
package/README.md
CHANGED
|
@@ -12,15 +12,25 @@
|
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
<p align="center">
|
|
15
|
-
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge
|
|
15
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/🌐_DSH_Hub-goodandready.app-ff4500.svg?style=for-the-badge&labelColor=1a1a2e" alt="GoodAndReady Showcase"></a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
19
|
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
20
|
-
<a href="
|
|
21
|
-
<a href="
|
|
20
|
+
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
21
|
+
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
22
22
|
</p>
|
|
23
23
|
|
|
24
|
+
<table align="center">
|
|
25
|
+
<tr>
|
|
26
|
+
<td align="center">
|
|
27
|
+
⭐ <strong>If you like this plugin, please star it on GitHub</strong> — it shows me that the plugin is useful to you and motivates me to keep developing it.
|
|
28
|
+
<br><br>
|
|
29
|
+
🐛 <strong>If you find a bug or would like to request a feature</strong>, open a GitHub issue in any language — I will review your proposal and implement useful suggestions in a future plugin version.
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
</table>
|
|
33
|
+
|
|
24
34
|
</div>
|
|
25
35
|
|
|
26
36
|
---
|
|
@@ -64,15 +74,15 @@ graph TD
|
|
|
64
74
|
Audit["Audit Mode<br/>(Emits ctx.logger warning only)"]
|
|
65
75
|
end
|
|
66
76
|
|
|
67
|
-
Turn -->|
|
|
77
|
+
Turn -->|llm/stream hook| Hook
|
|
68
78
|
Hook --> ScopeCheck
|
|
69
79
|
ScopeCheck -->|No| PassThrough
|
|
70
80
|
ScopeCheck -->|Yes| Buffer
|
|
71
81
|
PassThrough --> ChatUI
|
|
72
82
|
Buffer --> Detector
|
|
73
83
|
Detector -->|No Artifact| ChatUI
|
|
74
|
-
Detector -->|Artifact
|
|
75
|
-
Detector -->|Artifact
|
|
84
|
+
Detector -->|Artifact detected: sanitize| Sanitize --> ChatUI
|
|
85
|
+
Detector -->|Artifact detected: audit| Audit --> ChatUI
|
|
76
86
|
```
|
|
77
87
|
|
|
78
88
|
---
|
|
@@ -151,7 +161,6 @@ npm run check
|
|
|
151
161
|
|
|
152
162
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
153
163
|
|
|
154
|
-
|
|
155
164
|
## Changed in v0.1.3
|
|
156
165
|
|
|
157
166
|
#8: wrap `llm/stream` in `ctx.effect` so unload unsubscribes.
|
|
@@ -162,3 +171,15 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
|
162
171
|
#11: schema `mode` default set to `sanitize` for out-of-the-box protection.
|
|
163
172
|
#11: robust open/close tag balancing in `sanitizeDsmlArtifacts` to handle prior closed blocks in prose.
|
|
164
173
|
#11: repo hygiene with tracked `package-lock.json` and design contract.
|
|
174
|
+
|
|
175
|
+
## Changed in v0.1.5
|
|
176
|
+
|
|
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.
|
|
@@ -12,15 +12,25 @@
|
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
<p align="center">
|
|
15
|
-
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge
|
|
15
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/🌐_DSH_Hub-goodandready.app-ff4500.svg?style=for-the-badge&labelColor=1a1a2e" alt="GoodAndReady Showcase"></a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
|
-
<a href="
|
|
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>
|
|
23
23
|
|
|
24
|
+
<table align="center">
|
|
25
|
+
<tr>
|
|
26
|
+
<td align="center">
|
|
27
|
+
⭐ <strong>Если вам нравится этот плагин, поставьте ему звезду на GitHub</strong> — это покажет мне, что плагин вам полезен, и будет мотивировать меня развивать его дальше.
|
|
28
|
+
<br><br>
|
|
29
|
+
🐛 <strong>Если вы нашли баг или хотите предложить новый функционал</strong>, создайте issue на GitHub на любом языке — я рассмотрю ваше предложение и реализую полезные идеи в одной из следующих версий плагина.
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
</table>
|
|
33
|
+
|
|
24
34
|
</div>
|
|
25
35
|
|
|
26
36
|
---
|
|
@@ -64,15 +74,15 @@ graph TD
|
|
|
64
74
|
Audit["Режим Audit<br/>(Логирование через ctx.logger)"]
|
|
65
75
|
end
|
|
66
76
|
|
|
67
|
-
Turn
|
|
77
|
+
Turn -->|Хук llm/stream| Hook
|
|
68
78
|
Hook --> ScopeCheck
|
|
69
79
|
ScopeCheck -->|Нет| PassThrough
|
|
70
80
|
ScopeCheck -->|Да| Buffer
|
|
71
81
|
PassThrough --> ChatUI
|
|
72
82
|
Buffer --> Detector
|
|
73
83
|
Detector -->|Нет артефакта| ChatUI
|
|
74
|
-
Detector -->|Артефакт
|
|
75
|
-
Detector -->|Артефакт
|
|
84
|
+
Detector -->|Артефакт найден: sanitize| Sanitize --> ChatUI
|
|
85
|
+
Detector -->|Артефакт найден: audit| Audit --> ChatUI
|
|
76
86
|
```
|
|
77
87
|
|
|
78
88
|
---
|
|
@@ -151,7 +161,6 @@ npm run check
|
|
|
151
161
|
|
|
152
162
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
153
163
|
|
|
154
|
-
|
|
155
164
|
## Changed in v0.1.3
|
|
156
165
|
|
|
157
166
|
#8: wrap `llm/stream` in `ctx.effect` so unload unsubscribes.
|
|
@@ -162,3 +171,15 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
|
162
171
|
#11: значение `mode` по умолчанию в схеме переведено на `sanitize` для защиты «из коробки».
|
|
163
172
|
#11: балансировка открывающих и закрывающих тегов в `sanitizeDsmlArtifacts` при наличии закрытых блоков в прозе.
|
|
164
173
|
#11: гигиена репозитория с отслеживаемым `package-lock.json` и дизайн-контрактом.
|
|
174
|
+
|
|
175
|
+
## Изменения в v0.1.5
|
|
176
|
+
|
|
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.
|
|
@@ -16,11 +16,21 @@
|
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
|
-
<a href="
|
|
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>
|
|
23
23
|
|
|
24
|
+
<table align="center">
|
|
25
|
+
<tr>
|
|
26
|
+
<td align="center">
|
|
27
|
+
⭐ <strong>如果您喜欢这个插件,请在 GitHub 上为它点亮 Star</strong> — 这能让我知道插件对您有用,并鼓励我继续开发和维护它。
|
|
28
|
+
<br><br>
|
|
29
|
+
🐛 <strong>如果您发现 Bug 或希望增加功能</strong>,请使用任意语言在 GitHub 上提交 Issue — 我会评估您的建议,并在后续版本中实现有价值的改进。
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
</table>
|
|
33
|
+
|
|
24
34
|
</div>
|
|
25
35
|
|
|
26
36
|
---
|
|
@@ -64,15 +74,15 @@ graph TD
|
|
|
64
74
|
Audit["审计模式 (Audit)<br/>(仅记录日志不改动文本)"]
|
|
65
75
|
end
|
|
66
76
|
|
|
67
|
-
Turn -->|
|
|
77
|
+
Turn -->|llm/stream 拦截| Hook
|
|
68
78
|
Hook --> ScopeCheck
|
|
69
79
|
ScopeCheck -->|未命中| PassThrough
|
|
70
80
|
ScopeCheck -->|命中| Buffer
|
|
71
81
|
PassThrough --> ChatUI
|
|
72
82
|
Buffer --> Detector
|
|
73
83
|
Detector -->|无异常| ChatUI
|
|
74
|
-
Detector
|
|
75
|
-
Detector
|
|
84
|
+
Detector -->|检出残留: sanitize| Sanitize --> ChatUI
|
|
85
|
+
Detector -->|检出残留: audit| Audit --> ChatUI
|
|
76
86
|
```
|
|
77
87
|
|
|
78
88
|
---
|
|
@@ -151,7 +161,6 @@ npm run check
|
|
|
151
161
|
|
|
152
162
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
153
163
|
|
|
154
|
-
|
|
155
164
|
## Changed in v0.1.3
|
|
156
165
|
|
|
157
166
|
#8: wrap `llm/stream` in `ctx.effect` so unload unsubscribes.
|
|
@@ -162,3 +171,15 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
|
162
171
|
#11: 将 schema 中 `mode` 默认值设置为 `sanitize`,实现开箱即用的清理保护。
|
|
163
172
|
#11: 在 `sanitizeDsmlArtifacts` 中实现严格的开闭标签深度平衡,精准处理正文中已闭合标签与末尾泄漏标签。
|
|
164
173
|
#11: 纳入 `package-lock.json` 跟踪并完善设计契约。
|
|
174
|
+
|
|
175
|
+
## v0.1.5 变更说明
|
|
176
|
+
|
|
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 打包归档。
|
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, rgba(255,255,255,0.08))',
|
|
250
|
+
background: 'var(--dsw-alias-bg-card, rgba(255,255,255,0.02))',
|
|
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, #f1f5f9)',
|
|
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: 'rgba(59,130,246,0.15)',
|
|
284
|
+
color: 'var(--dsw-alias-accent-primary, #3b82f6)',
|
|
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: 'rgba(239,68,68,0.15)',
|
|
294
|
+
color: '#ef4444',
|
|
295
|
+
},
|
|
296
|
+
}, 'OFF')
|
|
297
|
+
),
|
|
298
|
+
React.createElement('span', {
|
|
299
|
+
style: {
|
|
300
|
+
fontSize: 13,
|
|
301
|
+
color: 'var(--dsw-alias-label-secondary, #94a3b8)',
|
|
302
|
+
},
|
|
303
|
+
}, tt('sub'))
|
|
304
|
+
),
|
|
305
|
+
React.createElement('div', {
|
|
306
|
+
style: {
|
|
307
|
+
display: 'flex',
|
|
308
|
+
alignItems: 'center',
|
|
309
|
+
color: 'var(--dsw-alias-label-tertiary, #64748b)',
|
|
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, rgba(255,255,255,0.08))',
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
// One-Click Updater block
|
|
326
|
+
React.createElement('div', {
|
|
327
|
+
style: {
|
|
328
|
+
padding: '12px 14px',
|
|
329
|
+
margin: '14px 0',
|
|
330
|
+
background: 'rgba(255,255,255,0.03)',
|
|
331
|
+
borderRadius: 8,
|
|
332
|
+
border: '1px solid var(--dsw-alias-border-l2, rgba(255,255,255,0.08))',
|
|
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, #94a3b8)' } },
|
|
342
|
+
tt('version') + ': ' + updateState.currentVersion
|
|
343
|
+
),
|
|
344
|
+
updateState.checking && React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary, #64748b)', fontSize: 12 } },
|
|
345
|
+
tt('updateChecking')
|
|
346
|
+
),
|
|
347
|
+
!updateState.checking && updateState.updateAvailable && React.createElement('span', { style: { color: '#f59e0b', fontWeight: 600, fontSize: 12 } },
|
|
348
|
+
tt('updateAvailable', { v: updateState.latestVersion })
|
|
349
|
+
),
|
|
350
|
+
!updateState.checking && !updateState.updateAvailable && !updateState.error && React.createElement('span', { style: { color: '#10b981', fontSize: 12 } },
|
|
351
|
+
'✓ ' + tt('upToDate')
|
|
352
|
+
),
|
|
353
|
+
updateState.error && React.createElement('span', { style: { color: '#ef4444', 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-accent-primary, #3b82f6)',
|
|
364
|
+
color: '#fff',
|
|
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: 'rgba(16,185,129,0.1)',
|
|
377
|
+
color: '#10b981',
|
|
378
|
+
borderRadius: 6,
|
|
379
|
+
fontSize: 12,
|
|
380
|
+
border: '1px solid rgba(16,185,129,0.2)',
|
|
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: 'rgba(239,68,68,0.1)',
|
|
390
|
+
color: '#f87171',
|
|
391
|
+
borderRadius: 6,
|
|
392
|
+
fontSize: 12,
|
|
393
|
+
border: '1px solid rgba(239,68,68,0.2)',
|
|
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, #64748b)' },
|
|
400
|
+
}, tt('saving')),
|
|
401
|
+
status === 'unavailable' && React.createElement('div', {
|
|
402
|
+
style: { padding: '16px 0', fontSize: 13, color: '#ef4444' },
|
|
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, #f1f5f9)' },
|
|
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, rgba(255,255,255,0.08))',
|
|
419
|
+
background: 'var(--dsw-alias-bg-field, rgba(0,0,0,0.2))',
|
|
420
|
+
color: 'var(--dsw-alias-label-primary, #f1f5f9)',
|
|
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, #f1f5f9)' },
|
|
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, rgba(255,255,255,0.08))',
|
|
444
|
+
background: 'var(--dsw-alias-bg-field, rgba(0,0,0,0.2))',
|
|
445
|
+
color: 'var(--dsw-alias-label-primary, #f1f5f9)',
|
|
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, #f1f5f9)' },
|
|
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, rgba(255,255,255,0.08))',
|
|
465
|
+
background: 'var(--dsw-alias-bg-field, rgba(0,0,0,0.2))',
|
|
466
|
+
color: 'var(--dsw-alias-label-primary, #f1f5f9)',
|
|
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, rgba(255,255,255,0.08))',
|
|
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-accent-primary, #3b82f6)',
|
|
493
|
+
color: '#fff',
|
|
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') ? '#10b981' : '#f87171',
|
|
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
|
-
|
|
20
|
-
|
|
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,
|
|
94
|
-
const
|
|
95
|
-
|
|
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
|
|
136
|
+
return current.mode === 'disabled' || !matchesScope(o, current)
|
|
99
137
|
? s
|
|
100
138
|
: sanitizeDsmlArtifacts(s, {
|
|
101
|
-
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
-
"
|
|
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
|
}
|
package/docs/design/DESIGN.md
DELETED
|
@@ -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`).
|