@goodandready/dsh-dsml-artifact-guard 0.2.12 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -107
- package/README.md +22 -100
- package/README.ru.md +27 -81
- package/README.zh.md +33 -87
- package/lib/client.js +303 -20
- package/lib/index.js +118 -8
- package/lib/telemetry.js +128 -0
- package/lib/updater.js +6 -1
- package/package.json +2 -2
package/README.zh.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
5
|
-
<h3
|
|
5
|
+
<h3>零依赖流式防护与清理插件:拦截 DeepSeek DSML 协议泄漏闭合标签及思维链残留标记</h3>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<a href="https://www.npmjs.com/package/@goodandready/dsh-dsml-artifact-guard"><img src="https://img.shields.io/npm/v/@goodandready/dsh-dsml-artifact-guard.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
@@ -12,7 +12,7 @@
|
|
|
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">
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
<table align="center">
|
|
25
25
|
<tr>
|
|
26
26
|
<td align="center">
|
|
27
|
-
⭐ <strong
|
|
27
|
+
⭐ <strong>如果这个插件对您有所帮助,请在 GitHub 上点亮 Star</strong> — 您的支持将激励我持续维护与完善。
|
|
28
28
|
<br><br>
|
|
29
|
-
🐛 <strong
|
|
29
|
+
🐛 <strong>发现缺陷或有功能建议?</strong> 欢迎在 GitHub 提交 Issue。
|
|
30
30
|
</td>
|
|
31
31
|
</tr>
|
|
32
32
|
</table>
|
|
@@ -35,88 +35,25 @@
|
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
-
## ⚡
|
|
38
|
+
## ⚡ 解决的问题与功能亮点
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
在通过第三方代理网关调用 DeepSeek 模型时,工具调用协议(DSML — DeepSeek Markup Language)的闭合标签有时会泄漏至可见文本流中:
|
|
41
41
|
|
|
42
42
|
```text
|
|
43
|
-
|
|
43
|
+
任务已完成。
|
|
44
44
|
</|DSML|parameter> </|DSML|invoke> </|DSML|tool_calls>
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
此外,推理模型(如 DeepSeek-R1 / V3)在异常中断时可能残留游离的 `</think>` 标签、末端标记或未闭合的思维块,破坏 Markdown 排版。
|
|
48
48
|
|
|
49
|
-
**`@goodandready/dsh-dsml-artifact-guard`**
|
|
49
|
+
**`@goodandready/dsh-dsml-artifact-guard`** 实时拦截并清洗这些协议残留物:
|
|
50
50
|
|
|
51
|
-
1.
|
|
52
|
-
2.
|
|
53
|
-
3.
|
|
54
|
-
4.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
## 💡 为什么核心层未包含此功能?
|
|
59
|
-
|
|
60
|
-
截至 DeepSeek Harness **0.1.7-rc.2**,核心上游流管道(`dsh-llm-pi-ai`、`dsh-llm-deepseek*` 等)均未对末端 DSML 协议标签进行任何清洗——核心引擎中完全不存在对 `|DSML|` 的处理。当上游网关(例如 `commandcode`、`deepseek-official` 或其他代理)在回复结尾泄漏协议闭合标签时,DSH 会将其直接传输到聊天前端。
|
|
61
|
-
|
|
62
|
-
`dsh-dsml-artifact-guard` 为所有 DeepSeek 模型提供通用的运行时防护,不受特定服务商网关或路由配置的限制。
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## 🏗️ 架构设计
|
|
67
|
-
|
|
68
|
-
```mermaid
|
|
69
|
-
graph TD
|
|
70
|
-
subgraph DSH ["DeepSeek Harness 核心运行时"]
|
|
71
|
-
Turn["智能体回合执行<br/>(发起大模型流式请求)"]
|
|
72
|
-
ChatUI["聊天界面流式消费端<br/>(渲染纯净 Markdown 文本)"]
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
subgraph Guard ["@goodandready/dsh-dsml-artifact-guard"]
|
|
76
|
-
Hook["同步 llm/stream 拦截钩子<br/>(同步返回 AsyncIterable)"]
|
|
77
|
-
ScopeCheck{"是否命中范围?<br/>(校验 providerId 与 modelId)"}
|
|
78
|
-
PassThrough["原始数据流直通<br/>(其他模型零延迟零开销)"]
|
|
79
|
-
Buffer["滑动尾部缓冲区<br/>(跨分片保留最后 96 字节)"]
|
|
80
|
-
Detector{"末端残留判定?<br/>(流结束 finish 时比对标签)"]
|
|
81
|
-
Sanitize["清洗模式 (Sanitize)<br/>(安全剥除协议残留标签)"]
|
|
82
|
-
Audit["审计模式 (Audit)<br/>(仅记录日志不改动文本)"]
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
Turn -->|llm/stream 拦截| Hook
|
|
86
|
-
Hook --> ScopeCheck
|
|
87
|
-
ScopeCheck -->|未命中| PassThrough
|
|
88
|
-
ScopeCheck -->|命中| Buffer
|
|
89
|
-
PassThrough --> ChatUI
|
|
90
|
-
Buffer --> Detector
|
|
91
|
-
Detector -->|无异常| ChatUI
|
|
92
|
-
Detector -->|检出残留: sanitize| Sanitize --> ChatUI
|
|
93
|
-
Detector -->|检出残留: audit| Audit --> ChatUI
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
## ✨ 核心特性深度解析
|
|
99
|
-
|
|
100
|
-
### 1. 同步 Hook 契约保护
|
|
101
|
-
在 Cordis 与 DSH 的底层事件总线中,`llm/stream` 监听器必须同步返回转换后的迭代器。若将回调函数声明为 `async`,返回值将被自动包裹为 `Promise<AsyncIterable>`,导致下层调度器抛出致命异常:`TypeError: stream is not async iterable`。`dsh-dsml-artifact-guard` 采用纯同步生成器封装,从根源规避调度崩溃。
|
|
102
|
-
|
|
103
|
-
### 2. 跨分片长尾标签精准捕获
|
|
104
|
-
在实际网络传输中,长尾标签 `</|DSML|parameter> </|DSML|invoke> </|DSML|tool_calls>` 常被拆分为多个 chunk:
|
|
105
|
-
* Chunk 1: `All tasks complete. </|DSML|pa`
|
|
106
|
-
* Chunk 2: `rameter> </|DSML|invoke> `
|
|
107
|
-
* Chunk 3: `</|DSML|tool_calls>`
|
|
108
|
-
|
|
109
|
-
滑动窗口在接收到下一个文本分片或 `finish` 信号前保留尾部 96 字节,确保跨包切分的标签被作为一个完整实体精准匹配并剔除。
|
|
110
|
-
|
|
111
|
-
### 3. 数据安全与故障开放保障
|
|
112
|
-
* 正文技术讨论中出现 DSML 示例代码时(如 `<|DSML|tool_calls>demo</|DSML|tool_calls>`),绝不误删。
|
|
113
|
-
* 非文本分片(`tool-call-delta`, `usage`, `finish`)即时透传,无任何额外排队延迟。
|
|
114
|
-
* 遇到畸形数据格式时自动降级放行,优先确保会话畅通不中断。
|
|
115
|
-
|
|
116
|
-
### 4. 三种灵活运行模式
|
|
117
|
-
* **`sanitize`** *(默认)*:自动剥离末尾残留标签,并在日志中输出剥离计数。
|
|
118
|
-
* **`audit`**:仅通过 `ctx.logger.info(...)` 记录检出警告,保持用户界面原样输出。
|
|
119
|
-
* **`disabled`**:完全旁路,不进行任何处理。
|
|
51
|
+
1. **严格同步钩子契约**:同步实现 `llm/stream` 钩子,直接返回 `AsyncIterable`,避免异步 Promise 导致的崩溃。
|
|
52
|
+
2. **分片边界滑动窗口**:通过 96 字节滑动窗口完整捕获跨网络分片截断的闭合标签。
|
|
53
|
+
3. **连接异常中断残缺清理**:流收到 `finish` 信号时自动剥离残缺的协议前缀(如 `</|DSML|...`)。
|
|
54
|
+
4. **思维链标记防护 (`cleanThinkArtifacts`)**:清理孤立的 `</think>` 标签、末端思维标记,并在流中断时自动修复未闭合的思维块。
|
|
55
|
+
5. **标签平衡与故障开放**:追踪开闭标签平衡,技术讨论中的合法 DSML 样例绝不被误删。
|
|
56
|
+
6. **实时遥测与拦截统计面板**:在 Web UI 设置卡片中直观展示已防护应答数、剥除标签数、最近拦截记录与重置操作。
|
|
120
57
|
|
|
121
58
|
---
|
|
122
59
|
|
|
@@ -134,14 +71,15 @@ dsh plugin --profile web add @goodandready/dsh-dsml-artifact-guard
|
|
|
134
71
|
|
|
135
72
|
## ⚙️ 配置指南 (`settings.yaml`)
|
|
136
73
|
|
|
137
|
-
在 `settings.yaml` 或 Web UI
|
|
74
|
+
在 `settings.yaml` 或 Web UI 设置面板中配置规则:
|
|
138
75
|
|
|
139
76
|
```yaml
|
|
140
77
|
# settings.yaml
|
|
141
78
|
dsh-dsml-artifact-guard:
|
|
142
79
|
mode: sanitize
|
|
143
|
-
|
|
144
|
-
|
|
80
|
+
modelPattern: "deepseek"
|
|
81
|
+
providers: []
|
|
82
|
+
cleanThinkArtifacts: false
|
|
145
83
|
```
|
|
146
84
|
|
|
147
85
|
### 配置参数参考表
|
|
@@ -149,8 +87,20 @@ dsh-dsml-artifact-guard:
|
|
|
149
87
|
| 参数名 | 类型 | 默认值 | 功能说明 |
|
|
150
88
|
|:---|:---|:---|:---|
|
|
151
89
|
| `mode` | `string` | `"sanitize"` | 运行模式:`"sanitize"`(清洗标签)、`"audit"`(仅审计)或 `"disabled"` |
|
|
152
|
-
| `
|
|
153
|
-
| `
|
|
90
|
+
| `modelPattern` | `string` | `"deepseek"` | 不区分大小写的模型正则匹配式(默认:`"deepseek"`) |
|
|
91
|
+
| `providers` | `string[]` | `[]` | 目标服务商列表。为空时防护全部服务商 |
|
|
92
|
+
| `cleanThinkArtifacts` | `boolean` | `false` | 剥除游离 `</think>` 标签、末端思维标记并修复未闭合思维块 |
|
|
93
|
+
| `providerId` | `string` | `undefined` | *(已弃用)* 目标服务商标识符;保留用于向后兼容 |
|
|
94
|
+
| `modelId` | `string` | `undefined` | *(已弃用)* 目标模型标识符;保留用于向后兼容 |
|
|
95
|
+
|
|
96
|
+
### HTTP 管理接口
|
|
97
|
+
|
|
98
|
+
| 方法 | 接口路径 | 访问权限 | 说明 |
|
|
99
|
+
|:---|:---|:---|:---|
|
|
100
|
+
| `GET` | `/api/dsh-dsml-artifact-guard/stats` | Web UI / Localhost | 获取实时拦截遥测与最近事件列表 |
|
|
101
|
+
| `POST` | `/api/dsh-dsml-artifact-guard/stats` | Loopback & Same-Origin | 重置遥测计数器并清空最近事件 |
|
|
102
|
+
| `GET` | `/api/dsh-dsml-artifact-guard/update` | Web UI / Localhost | 检查当前版本与最新版本状态 |
|
|
103
|
+
| `POST` | `/api/dsh-dsml-artifact-guard/update` | Loopback & Same-Origin | 执行原地热更新 |
|
|
154
104
|
|
|
155
105
|
---
|
|
156
106
|
|
|
@@ -168,7 +118,3 @@ npm run check
|
|
|
168
118
|
## 📄 开源许可证
|
|
169
119
|
|
|
170
120
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
171
|
-
|
|
172
|
-
---
|
|
173
|
-
|
|
174
|
-
完整版本演进与更新记录详见 [CHANGELOG.md](CHANGELOG.md)。
|
package/lib/client.js
CHANGED
|
@@ -46,6 +46,19 @@ window.__ModuleLoader__.load({
|
|
|
46
46
|
providers: 'Target Providers (optional)',
|
|
47
47
|
providersPlaceholder: 'all providers (comma-separated, e.g. commandcode, deepseek-official)',
|
|
48
48
|
providersHint: 'Comma-separated list of provider IDs. Leave empty to guard all providers.',
|
|
49
|
+
cleanThinkArtifacts: 'Clean reasoning (<think>) artifacts & incomplete tokens',
|
|
50
|
+
cleanThinkArtifactsHint: 'Strip orphan </think> tags, trailing reasoning markers, and incomplete closing tokens at stream finish.',
|
|
51
|
+
telemetryTitle: 'Activity & Telemetry',
|
|
52
|
+
telemetryGuarded: 'Guarded responses',
|
|
53
|
+
telemetryRemoved: 'Stripped tags',
|
|
54
|
+
telemetryLast: 'Last incident',
|
|
55
|
+
telemetryNever: 'None yet',
|
|
56
|
+
telemetryReset: 'Reset',
|
|
57
|
+
telemetryResetting: 'Resetting…',
|
|
58
|
+
telemetryRecent: 'Recent Incidents',
|
|
59
|
+
telemetryNoRecent: 'No recent incidents recorded',
|
|
60
|
+
telemetryShowLog: 'View incident log',
|
|
61
|
+
telemetryHideLog: 'Hide incident log',
|
|
49
62
|
legacyDetected: 'Legacy providerId / modelId detected: active as an additional rule.',
|
|
50
63
|
legacyClear: 'Clear legacy fields',
|
|
51
64
|
providerId: 'Target Provider ID (legacy)',
|
|
@@ -78,24 +91,37 @@ window.__ModuleLoader__.load({
|
|
|
78
91
|
modelPatternHint: '对模型 ID 进行不区分大小写的正则表达式匹配(默认:deepseek)。支持任意服务商前缀。',
|
|
79
92
|
providers: '目标服务商(可选)',
|
|
80
93
|
providersPlaceholder: '所有服务商(以逗号分隔,例如 commandcode, deepseek-official)',
|
|
81
|
-
providersHint: '
|
|
82
|
-
|
|
94
|
+
providersHint: '服务商 ID 列表(逗号分隔)。留空则拦截所有服务商。',
|
|
95
|
+
cleanThinkArtifacts: '清洗思维链 (<think>) 残留与末端残缺标记',
|
|
96
|
+
cleanThinkArtifactsHint: '自动剥除游离的 </think> 闭合标签、末端思维标记,并在流异常中断时清理残缺协议标记。',
|
|
97
|
+
telemetryTitle: '运行遥测与拦截统计',
|
|
98
|
+
telemetryGuarded: '已防护应答',
|
|
99
|
+
telemetryRemoved: '已剥除标签',
|
|
100
|
+
telemetryLast: '最近拦截',
|
|
101
|
+
telemetryNever: '暂无记录',
|
|
102
|
+
telemetryReset: '重置统计',
|
|
103
|
+
telemetryResetting: '正在重置…',
|
|
104
|
+
telemetryRecent: '最近拦截记录',
|
|
105
|
+
telemetryNoRecent: '暂无拦截记录',
|
|
106
|
+
telemetryShowLog: '查看记录',
|
|
107
|
+
telemetryHideLog: '收起记录',
|
|
108
|
+
legacyDetected: '检测到旧版 providerId / modelId:将作为附加规则生效。',
|
|
83
109
|
legacyClear: '清除旧版配置',
|
|
84
|
-
providerId: '
|
|
85
|
-
modelId: '
|
|
86
|
-
disabledNotice: '⚠️
|
|
87
|
-
statusUnavailable: '
|
|
110
|
+
providerId: '目标服务商 ID(旧版)',
|
|
111
|
+
modelId: '目标模型 ID(旧版)',
|
|
112
|
+
disabledNotice: '⚠️ 守卫当前处于禁用状态。DSML 闭合标签将直接进入聊天界面。',
|
|
113
|
+
statusUnavailable: '设置服务不可用。',
|
|
88
114
|
saving: '正在保存…',
|
|
89
|
-
ready: '
|
|
90
|
-
saved: '
|
|
115
|
+
ready: '保存设置',
|
|
116
|
+
saved: '设置已保存',
|
|
91
117
|
saveError: '保存失败: {msg}',
|
|
92
118
|
};
|
|
93
119
|
|
|
94
|
-
|
|
95
120
|
const DEFAULTS = {
|
|
96
121
|
mode: 'sanitize',
|
|
97
122
|
modelPattern: 'deepseek',
|
|
98
123
|
providers: '',
|
|
124
|
+
cleanThinkArtifacts: false,
|
|
99
125
|
providerId: '',
|
|
100
126
|
modelId: '',
|
|
101
127
|
};
|
|
@@ -107,11 +133,23 @@ window.__ModuleLoader__.load({
|
|
|
107
133
|
const [saving, setSaving] = React.useState(false);
|
|
108
134
|
const [msg, setMsg] = React.useState('');
|
|
109
135
|
|
|
136
|
+
// Telemetry state
|
|
137
|
+
const [telemetry, setTelemetry] = React.useState({
|
|
138
|
+
totalCleaned: 0,
|
|
139
|
+
sessionsGuarded: 0,
|
|
140
|
+
lastCleanedAt: null,
|
|
141
|
+
lastModel: null,
|
|
142
|
+
lastProvider: null,
|
|
143
|
+
recent: [],
|
|
144
|
+
resetting: false,
|
|
145
|
+
});
|
|
146
|
+
const [showRecentLog, setShowRecentLog] = React.useState(false);
|
|
147
|
+
|
|
110
148
|
// Updater state
|
|
111
149
|
const [updateState, setUpdateState] = React.useState({
|
|
112
150
|
checking: false,
|
|
113
151
|
updateAvailable: false,
|
|
114
|
-
currentVersion: '0.
|
|
152
|
+
currentVersion: '0.2.13',
|
|
115
153
|
latestVersion: '',
|
|
116
154
|
updating: false,
|
|
117
155
|
notice: '',
|
|
@@ -193,6 +231,7 @@ window.__ModuleLoader__.load({
|
|
|
193
231
|
providers: Array.isArray(view.providers)
|
|
194
232
|
? view.providers.join(', ')
|
|
195
233
|
: (typeof view.providers === 'string' ? view.providers : ''),
|
|
234
|
+
cleanThinkArtifacts: Boolean(view.cleanThinkArtifacts),
|
|
196
235
|
providerId: view.providerId ? String(view.providerId) : '',
|
|
197
236
|
modelId: view.modelId ? String(view.modelId) : '',
|
|
198
237
|
});
|
|
@@ -232,6 +271,78 @@ window.__ModuleLoader__.load({
|
|
|
232
271
|
};
|
|
233
272
|
}, [scope]);
|
|
234
273
|
|
|
274
|
+
// Fetch telemetry
|
|
275
|
+
const fetchTelemetry = React.useCallback(async () => {
|
|
276
|
+
try {
|
|
277
|
+
const res = await fetch('/api/dsh-dsml-artifact-guard/stats');
|
|
278
|
+
if (!res.ok) return;
|
|
279
|
+
const data = await res.json();
|
|
280
|
+
setTelemetry(prev => ({
|
|
281
|
+
...prev,
|
|
282
|
+
totalCleaned: data.totalCleaned || 0,
|
|
283
|
+
sessionsGuarded: data.sessionsGuarded || 0,
|
|
284
|
+
lastCleanedAt: data.lastCleanedAt || null,
|
|
285
|
+
lastModel: data.lastModel || null,
|
|
286
|
+
lastProvider: data.lastProvider || null,
|
|
287
|
+
recent: Array.isArray(data.recent) ? data.recent : [],
|
|
288
|
+
}));
|
|
289
|
+
} catch (e) {
|
|
290
|
+
/* Ignore telemetry poll errors */
|
|
291
|
+
}
|
|
292
|
+
}, []);
|
|
293
|
+
|
|
294
|
+
React.useEffect(() => {
|
|
295
|
+
fetchTelemetry();
|
|
296
|
+
}, [fetchTelemetry]);
|
|
297
|
+
|
|
298
|
+
React.useEffect(() => {
|
|
299
|
+
if (expanded) {
|
|
300
|
+
fetchTelemetry();
|
|
301
|
+
}
|
|
302
|
+
}, [expanded, fetchTelemetry]);
|
|
303
|
+
|
|
304
|
+
const onResetTelemetry = async () => {
|
|
305
|
+
if (telemetry.resetting) return;
|
|
306
|
+
setTelemetry(s => ({ ...s, resetting: true }));
|
|
307
|
+
try {
|
|
308
|
+
const res = await fetch('/api/dsh-dsml-artifact-guard/stats', {
|
|
309
|
+
method: 'POST',
|
|
310
|
+
headers: {
|
|
311
|
+
'content-type': 'application/json',
|
|
312
|
+
'x-dsh-plugin-stats': '1',
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
if (res.ok) {
|
|
316
|
+
const data = await res.json();
|
|
317
|
+
const updated = data.stats || {};
|
|
318
|
+
setTelemetry({
|
|
319
|
+
totalCleaned: updated.totalCleaned || 0,
|
|
320
|
+
sessionsGuarded: updated.sessionsGuarded || 0,
|
|
321
|
+
lastCleanedAt: updated.lastCleanedAt || null,
|
|
322
|
+
lastModel: updated.lastModel || null,
|
|
323
|
+
lastProvider: updated.lastProvider || null,
|
|
324
|
+
recent: Array.isArray(updated.recent) ? updated.recent : [],
|
|
325
|
+
resetting: false,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
} catch (e) {
|
|
329
|
+
/* Ignore reset error */
|
|
330
|
+
} finally {
|
|
331
|
+
setTelemetry(s => ({ ...s, resetting: false }));
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const formatLastActivity = (iso) => {
|
|
336
|
+
if (!iso) return tt('telemetryNever');
|
|
337
|
+
try {
|
|
338
|
+
const d = new Date(iso);
|
|
339
|
+
if (isNaN(d.getTime())) return tt('telemetryNever');
|
|
340
|
+
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
341
|
+
} catch (e) {
|
|
342
|
+
return tt('telemetryNever');
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
235
346
|
// Check updater status on mount
|
|
236
347
|
React.useEffect(() => {
|
|
237
348
|
let cancelled = false;
|
|
@@ -299,6 +410,7 @@ window.__ModuleLoader__.load({
|
|
|
299
410
|
['mode', draft.mode || 'sanitize'],
|
|
300
411
|
['modelPattern', (draft.modelPattern !== undefined && draft.modelPattern !== null) ? String(draft.modelPattern).trim() : 'deepseek'],
|
|
301
412
|
['providers', draft.providers ? draft.providers.split(',').map(s => s.trim()).filter(Boolean) : []],
|
|
413
|
+
['cleanThinkArtifacts', Boolean(draft.cleanThinkArtifacts)],
|
|
302
414
|
['providerId', draft.providerId ? String(draft.providerId).trim() : ''],
|
|
303
415
|
['modelId', draft.modelId ? String(draft.modelId).trim() : ''],
|
|
304
416
|
];
|
|
@@ -386,9 +498,7 @@ window.__ModuleLoader__.load({
|
|
|
386
498
|
)
|
|
387
499
|
);
|
|
388
500
|
|
|
389
|
-
// Body.
|
|
390
|
-
// the title, icon and crumb and supplies its own padding, so rendering
|
|
391
|
-
// our card chrome there would double the border and shift the block.
|
|
501
|
+
// Body.
|
|
392
502
|
const body = React.createElement('div', {
|
|
393
503
|
style: {
|
|
394
504
|
padding: bare ? '0' : '0 16px 16px',
|
|
@@ -454,6 +564,153 @@ window.__ModuleLoader__.load({
|
|
|
454
564
|
},
|
|
455
565
|
}, updateState.notice),
|
|
456
566
|
|
|
567
|
+
// Telemetry Block
|
|
568
|
+
React.createElement('div', {
|
|
569
|
+
style: {
|
|
570
|
+
padding: '12px 14px',
|
|
571
|
+
marginBottom: 14,
|
|
572
|
+
background: 'color-mix(in srgb, var(--dsw-alias-label-primary) 3%, transparent)',
|
|
573
|
+
borderRadius: 8,
|
|
574
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
575
|
+
display: 'flex',
|
|
576
|
+
flexDirection: 'column',
|
|
577
|
+
gap: 8,
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
React.createElement('div', {
|
|
581
|
+
style: {
|
|
582
|
+
display: 'flex',
|
|
583
|
+
alignItems: 'center',
|
|
584
|
+
justifyContent: 'space-between',
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
React.createElement('span', {
|
|
588
|
+
style: { fontSize: 12, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' },
|
|
589
|
+
}, '📊 ' + tt('telemetryTitle')),
|
|
590
|
+
React.createElement('button', {
|
|
591
|
+
type: 'button',
|
|
592
|
+
onClick: onResetTelemetry,
|
|
593
|
+
disabled: telemetry.resetting,
|
|
594
|
+
style: {
|
|
595
|
+
padding: '3px 8px',
|
|
596
|
+
borderRadius: 4,
|
|
597
|
+
fontSize: 11,
|
|
598
|
+
border: 0,
|
|
599
|
+
background: 'color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent)',
|
|
600
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
601
|
+
cursor: telemetry.resetting ? 'default' : 'pointer',
|
|
602
|
+
},
|
|
603
|
+
}, telemetry.resetting ? tt('telemetryResetting') : tt('telemetryReset'))
|
|
604
|
+
),
|
|
605
|
+
React.createElement('div', {
|
|
606
|
+
style: {
|
|
607
|
+
display: 'grid',
|
|
608
|
+
gridTemplateColumns: 'repeat(auto-fit, minmax(110px, 1fr))',
|
|
609
|
+
gap: 8,
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
React.createElement('div', {
|
|
613
|
+
style: {
|
|
614
|
+
padding: '8px 10px',
|
|
615
|
+
borderRadius: 6,
|
|
616
|
+
background: 'color-mix(in srgb, var(--dsw-alias-label-primary) 4%, transparent)',
|
|
617
|
+
display: 'flex',
|
|
618
|
+
flexDirection: 'column',
|
|
619
|
+
gap: 2,
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
React.createElement('span', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)' } }, tt('telemetryGuarded')),
|
|
623
|
+
React.createElement('span', { style: { fontSize: 15, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, String(telemetry.sessionsGuarded))
|
|
624
|
+
),
|
|
625
|
+
React.createElement('div', {
|
|
626
|
+
style: {
|
|
627
|
+
padding: '8px 10px',
|
|
628
|
+
borderRadius: 6,
|
|
629
|
+
background: 'color-mix(in srgb, var(--dsw-alias-label-primary) 4%, transparent)',
|
|
630
|
+
display: 'flex',
|
|
631
|
+
flexDirection: 'column',
|
|
632
|
+
gap: 2,
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
React.createElement('span', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)' } }, tt('telemetryRemoved')),
|
|
636
|
+
React.createElement('span', { style: { fontSize: 15, fontWeight: 600, color: 'var(--dsw-alias-brand-primary, var(--dsw-alias-state-brand-primary))' } }, String(telemetry.totalCleaned))
|
|
637
|
+
),
|
|
638
|
+
React.createElement('div', {
|
|
639
|
+
style: {
|
|
640
|
+
padding: '8px 10px',
|
|
641
|
+
borderRadius: 6,
|
|
642
|
+
background: 'color-mix(in srgb, var(--dsw-alias-label-primary) 4%, transparent)',
|
|
643
|
+
display: 'flex',
|
|
644
|
+
flexDirection: 'column',
|
|
645
|
+
gap: 2,
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
React.createElement('span', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)' } }, tt('telemetryLast')),
|
|
649
|
+
React.createElement('span', { style: { fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-label-secondary)' } }, formatLastActivity(telemetry.lastCleanedAt))
|
|
650
|
+
)
|
|
651
|
+
),
|
|
652
|
+
telemetry.recent.length > 0 && React.createElement('div', {
|
|
653
|
+
style: {
|
|
654
|
+
display: 'flex',
|
|
655
|
+
flexDirection: 'column',
|
|
656
|
+
gap: 6,
|
|
657
|
+
marginTop: 4,
|
|
658
|
+
paddingTop: 8,
|
|
659
|
+
borderTop: '1px solid color-mix(in srgb, var(--dsw-alias-border-l2) 50%, transparent)',
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
React.createElement('button', {
|
|
663
|
+
type: 'button',
|
|
664
|
+
onClick: () => setShowRecentLog(v => !v),
|
|
665
|
+
style: {
|
|
666
|
+
background: 'transparent',
|
|
667
|
+
border: 0,
|
|
668
|
+
padding: 0,
|
|
669
|
+
fontSize: 11,
|
|
670
|
+
color: 'var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary))',
|
|
671
|
+
cursor: 'pointer',
|
|
672
|
+
textAlign: 'left',
|
|
673
|
+
textDecoration: 'underline',
|
|
674
|
+
},
|
|
675
|
+
}, showRecentLog ? tt('telemetryHideLog') : tt('telemetryShowLog') + ' (' + telemetry.recent.length + ')'),
|
|
676
|
+
showRecentLog && React.createElement('div', {
|
|
677
|
+
style: {
|
|
678
|
+
display: 'flex',
|
|
679
|
+
flexDirection: 'column',
|
|
680
|
+
gap: 4,
|
|
681
|
+
maxHeight: 140,
|
|
682
|
+
overflowY: 'auto',
|
|
683
|
+
fontSize: 11,
|
|
684
|
+
fontFamily: 'monospace',
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
telemetry.recent.map(ev =>
|
|
688
|
+
React.createElement('div', {
|
|
689
|
+
key: ev.id,
|
|
690
|
+
style: {
|
|
691
|
+
padding: '4px 6px',
|
|
692
|
+
borderRadius: 4,
|
|
693
|
+
background: 'color-mix(in srgb, var(--dsw-alias-label-primary) 4%, transparent)',
|
|
694
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
695
|
+
display: 'flex',
|
|
696
|
+
justifyContent: 'space-between',
|
|
697
|
+
alignItems: 'center',
|
|
698
|
+
gap: 6,
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
React.createElement('span', null, formatLastActivity(ev.timestamp) + ' ' + (ev.model ? '[' + ev.model + ']' : '')),
|
|
702
|
+
React.createElement('span', {
|
|
703
|
+
style: {
|
|
704
|
+
color: 'var(--dsw-alias-state-warning-primary)',
|
|
705
|
+
fontWeight: 600,
|
|
706
|
+
},
|
|
707
|
+
}, '-' + ev.removed + ' ' + ev.type)
|
|
708
|
+
)
|
|
709
|
+
)
|
|
710
|
+
)
|
|
711
|
+
)
|
|
712
|
+
),
|
|
713
|
+
|
|
457
714
|
// Disabled Warning Banner
|
|
458
715
|
isDisabled && React.createElement('div', {
|
|
459
716
|
style: {
|
|
@@ -551,6 +808,38 @@ window.__ModuleLoader__.load({
|
|
|
551
808
|
}, tt('providersHint'))
|
|
552
809
|
),
|
|
553
810
|
|
|
811
|
+
// Clean Think Artifacts Checkbox
|
|
812
|
+
React.createElement('div', {
|
|
813
|
+
style: {
|
|
814
|
+
display: 'flex',
|
|
815
|
+
alignItems: 'flex-start',
|
|
816
|
+
gap: 10,
|
|
817
|
+
padding: '8px 10px',
|
|
818
|
+
borderRadius: 6,
|
|
819
|
+
background: 'color-mix(in srgb, var(--dsw-alias-label-primary) 3%, transparent)',
|
|
820
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
821
|
+
},
|
|
822
|
+
},
|
|
823
|
+
React.createElement('input', {
|
|
824
|
+
type: 'checkbox',
|
|
825
|
+
id: 'clean-think-artifacts',
|
|
826
|
+
checked: draft.cleanThinkArtifacts,
|
|
827
|
+
onChange: (e) => setDraft(d => ({ ...d, cleanThinkArtifacts: e.target.checked })),
|
|
828
|
+
style: { marginTop: 2, cursor: 'pointer' },
|
|
829
|
+
}),
|
|
830
|
+
React.createElement('label', {
|
|
831
|
+
htmlFor: 'clean-think-artifacts',
|
|
832
|
+
style: { display: 'flex', flexDirection: 'column', gap: 2, cursor: 'pointer' },
|
|
833
|
+
},
|
|
834
|
+
React.createElement('span', {
|
|
835
|
+
style: { fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-label-primary)' },
|
|
836
|
+
}, tt('cleanThinkArtifacts')),
|
|
837
|
+
React.createElement('span', {
|
|
838
|
+
style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)', lineHeight: '1.4' },
|
|
839
|
+
}, tt('cleanThinkArtifactsHint'))
|
|
840
|
+
)
|
|
841
|
+
),
|
|
842
|
+
|
|
554
843
|
// Legacy Provider/Model notice if detected
|
|
555
844
|
(draft.providerId || draft.modelId) && React.createElement('div', {
|
|
556
845
|
style: {
|
|
@@ -648,8 +937,6 @@ window.__ModuleLoader__.load({
|
|
|
648
937
|
}
|
|
649
938
|
|
|
650
939
|
// View-aware entry for the host slots that carry a configuration page.
|
|
651
|
-
// 'plugins.row.config' asks for view:'summary' (the one-liner under the row
|
|
652
|
-
// title) and view:'page' (the form, bare). The older seat keeps the card.
|
|
653
940
|
function PluginItem(props) {
|
|
654
941
|
if (props && props.view === 'summary') {
|
|
655
942
|
const tBind = (props && props.ctx && typeof props.ctx.locale?.bind === 'function')
|
|
@@ -722,11 +1009,7 @@ window.__ModuleLoader__.load({
|
|
|
722
1009
|
);
|
|
723
1010
|
|
|
724
1011
|
// List seat (plugins.item): the seat the Plugins page really renders as the
|
|
725
|
-
// plugin's own page with its configuration
|
|
726
|
-
// and crumb and asks for view 'summary' (the card's one-liner) or view 'page'
|
|
727
|
-
// (the form), both served by PluginItem. The label is a static string on
|
|
728
|
-
// purpose: it is resolved while the page renders, and a locale lookup there
|
|
729
|
-
// would take the whole client batch down with it.
|
|
1012
|
+
// plugin's own page with its configuration.
|
|
730
1013
|
registerSlotWhenReady('plugins.item', () =>
|
|
731
1014
|
ctx.slots.register(
|
|
732
1015
|
{
|