@goodandready/dsh-dsml-artifact-guard 0.2.13 → 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/README.zh.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  <div align="center">
4
4
 
5
- <h3>面向 DeepSeek Harness 的残留 DSML 协议闭合标签流式熔断清洗引擎</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/作者全部项目-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="作者全部项目"></a>
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>如果您喜欢这个插件,请在 GitHub 上为它点亮 Star</strong> — 这能让我知道插件对您有用,并鼓励我继续开发和维护它。
27
+ ⭐ <strong>如果这个插件对您有所帮助,请在 GitHub 上点亮 Star</strong> — 您的支持将激励我持续维护与完善。
28
28
  <br><br>
29
- 🐛 <strong>如果您发现 Bug 或希望增加功能</strong>,请使用任意语言在 GitHub 上提交 Issue — 我会评估您的建议,并在后续版本中实现有价值的改进。
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
- 在对接特定第三方大模型服务商或代理网关时,底层工具调用协议 DSML(DeepSeek Markup Language)的闭合标签有时会异常泄漏并附着在模型输出的正文末尾。用户在聊天界面中经常看到多余的协议残留:
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
- 此类闭合标签不仅造成视觉污染、破坏 Markdown 排版渲染,还会干扰后续多智能体协作解析或文本复制导出。
47
+ 此外,推理模型(如 DeepSeek-R1 / V3)在异常中断时可能残留游离的 `</think>` 标签、末端标记或未闭合的思维块,破坏 Markdown 排版。
48
48
 
49
- **`@goodandready/dsh-dsml-artifact-guard`** 是专为 DeepSeek Harness 打造的原生宿主级(host-only)文本流拦截过滤插件,在文本到达前端界面之前实时侦测并净化残留闭合标签:
49
+ **`@goodandready/dsh-dsml-artifact-guard`** 实时拦截并清洗这些协议残留物:
50
50
 
51
- 1. **严格同步 Hook 契约保障**:Cordis 要求流拦截器必须同步返回 `AsyncIterable`。将拦截器写成 `async` 会返回 `Promise`,直接引发运行时崩溃:`stream is not async iterable`。本插件严格保持同步注册。
52
- 2. **分片分包滑动缓冲流**:网络分包常将闭合标签切断并分散在不同 Text Delta 中。插件内置 96 字节(`KEEP = 96`)滑动窗口,精准捕获并合并跨分片的长尾标签。
53
- 3. **100% 故障开放(Fail-Open)安全**:绝不误伤合法正文。对于讨论 DSML 语法的正常技术文本,插件保证原样放行。
54
- 4. **定向模型与服务商匹配**:仅对发生协议泄漏的目标 Provider/Model 执行流式清洗,其他模型流量直通零开销。
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
- providerId: "your-provider-id"
144
- modelId: "your-model-id"
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
- | `providerId` | `string` | `"opencode-go"` | 发生协议标签泄漏的目标服务商标识符 |
153
- | `modelId` | `string` | `"deepseek-v4-flash"` | 发生协议标签泄漏的目标模型标识符 |
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: '以逗号分隔的服务商 ID 列表。留空则保护所有服务商。',
82
- legacyDetected: '检测到旧版 providerId / modelId 配置,并作为附加精确规则生效。',
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: '目标服务商标识 (Provider ID, 旧版)',
85
- modelId: '目标模型标识 (Model ID, 旧版)',
86
- disabledNotice: '⚠️ 守卫当前已禁用,残留的 DSML 闭合标签将直接显示在聊天界面。',
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.1.5',
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. The row/page seat asks for the form alone: the host page draws
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 — the page draws the title, icon
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
  {