@goodandready/dsh-usage-guard 0.1.7 → 0.1.10
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 +45 -1
- package/README.ru.md +33 -0
- package/README.zh.md +10 -0
- package/lib/client.js +386 -44
- package/lib/index.js +127 -57
- package/lib/patch.js +23 -30
- package/lib/telemetry.js +111 -0
- package/lib/updater.js +266 -0
- package/lib/usage.js +89 -76
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -22,38 +22,91 @@ window.__ModuleLoader__.load({
|
|
|
22
22
|
repairDesc: 'Replace missing, non-numeric, or malformed counters with 0 before summing to prevent NaN history lockups.',
|
|
23
23
|
report: 'Log diagnostic warnings',
|
|
24
24
|
reportDesc: 'Log a detailed warning naming the session turn, step, and raw sample whenever damaged usage arrives.',
|
|
25
|
+
maxStepTokens: 'Token Spike Clamping Limit',
|
|
26
|
+
maxStepTokensDesc: 'Clamp abnormally huge step usage counters (e.g., > 1,000,000) before projection calculation to prevent integer overflow and corrupted session summaries. Set 0 to disable.',
|
|
25
27
|
stats_title: 'Protection Status',
|
|
26
28
|
stats_mode: 'Current Mode',
|
|
27
29
|
stats_mode_repair: 'Active Repair (Safe)',
|
|
28
30
|
stats_mode_report: 'Audit Only (Unsafe)',
|
|
29
31
|
stats_scope: 'Scope Target',
|
|
30
32
|
stats_scope_val: 'sessionProjections',
|
|
33
|
+
telemetry_title: 'Live Telemetry',
|
|
34
|
+
telemetry_rescued: 'Rescued Events',
|
|
35
|
+
telemetry_tokens: 'Fixed Tokens',
|
|
36
|
+
telemetry_clamped: 'Clamped Spikes',
|
|
37
|
+
telemetry_last_incident: 'Last Incident',
|
|
38
|
+
telemetry_no_incidents: 'No incidents detected (System healthy)',
|
|
39
|
+
telemetry_refresh: 'Refresh Telemetry',
|
|
40
|
+
telemetry_refreshing: 'Refreshing…',
|
|
41
|
+
updater_title: 'Plugin Version & Updates',
|
|
42
|
+
updater_current: 'Current Version',
|
|
43
|
+
updater_latest: 'Latest Version',
|
|
44
|
+
updater_checking: 'Checking…',
|
|
45
|
+
updater_check: 'Check for Updates',
|
|
46
|
+
updater_updating: 'Updating…',
|
|
47
|
+
updater_update_now: 'Update to {version}',
|
|
48
|
+
updater_up_to_date: 'Plugin is up to date',
|
|
49
|
+
updater_failed: 'Update check failed',
|
|
50
|
+
updater_success: 'Update successful. Please reload page.',
|
|
31
51
|
};
|
|
32
52
|
|
|
33
|
-
const
|
|
34
|
-
title: '
|
|
35
|
-
subtitle: '
|
|
36
|
-
header_desc: '
|
|
37
|
-
badge_active: '
|
|
38
|
-
badge_report_only: '
|
|
39
|
-
'settings.loading': '
|
|
40
|
-
'settings.unavailable': '
|
|
41
|
-
'settings.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
const zh = {
|
|
54
|
+
title: 'Token 用量防护',
|
|
55
|
+
subtitle: 'Token 用量清洗与会话防崩溃保护',
|
|
56
|
+
header_desc: '保护会话历史投影免受所有 LLM 提供商损坏或缺失的 Token 计数器影响。',
|
|
57
|
+
badge_active: '保护生效中',
|
|
58
|
+
badge_report_only: '仅审计',
|
|
59
|
+
'settings.loading': '正在加载 Usage Guard 设置…',
|
|
60
|
+
'settings.unavailable': '设置不可用(宿主命名空间尚未就绪)。',
|
|
61
|
+
'settings.unmonitored': '配置监听不可用。当前使用默认配置运行;变更需重启 DSH 生效。',
|
|
62
|
+
'settings.retry': '重试',
|
|
63
|
+
saved: '设置保存成功',
|
|
64
|
+
save: '保存更改',
|
|
65
|
+
saving: '正在保存…',
|
|
66
|
+
repair: '自动修复 Token 计数器',
|
|
67
|
+
repairDesc: '在求和前将缺失、非数值或格式错误的计数器替换为 0,防止 NaN 导致会话历史崩溃锁死。',
|
|
68
|
+
report: '记录诊断告警',
|
|
69
|
+
reportDesc: '每当收到损坏的用量数据时,记录详细警告并指出会话轮次、步骤和原始样本。',
|
|
70
|
+
maxStepTokens: '单步 Token 突增削峰阈值',
|
|
71
|
+
maxStepTokensDesc: '在进行投影计算前,对异常巨大的单步用量计数器(例如 > 1,000,000)进行削峰限制,防止整数溢出与会话汇总异常。设为 0 表示禁用。',
|
|
72
|
+
stats_title: '保护状态',
|
|
73
|
+
stats_mode: '当前模式',
|
|
74
|
+
stats_mode_repair: '主动修复(安全)',
|
|
75
|
+
stats_mode_report: '仅审计(不安全)',
|
|
76
|
+
stats_scope: '作用范围',
|
|
54
77
|
stats_scope_val: 'sessionProjections',
|
|
78
|
+
telemetry_title: '实时拦截指标',
|
|
79
|
+
telemetry_rescued: '已挽救异常事件',
|
|
80
|
+
telemetry_tokens: '已修复 Token 数',
|
|
81
|
+
telemetry_clamped: '削峰拦截次数',
|
|
82
|
+
telemetry_last_incident: '最近一次拦截事件',
|
|
83
|
+
telemetry_no_incidents: '未检测到拦截事件(系统运行健康)',
|
|
84
|
+
telemetry_refresh: '刷新指标',
|
|
85
|
+
telemetry_refreshing: '正在刷新…',
|
|
86
|
+
updater_title: '插件版本与在线更新',
|
|
87
|
+
updater_current: '当前版本',
|
|
88
|
+
updater_latest: '最新版本',
|
|
89
|
+
updater_checking: '正在检查…',
|
|
90
|
+
updater_check: '检查更新',
|
|
91
|
+
updater_updating: '正在更新…',
|
|
92
|
+
updater_update_now: '立即更新至 {version}',
|
|
93
|
+
updater_up_to_date: '已是最新版本',
|
|
94
|
+
updater_failed: '检查更新失败',
|
|
95
|
+
updater_success: '更新成功,请刷新页面。',
|
|
55
96
|
};
|
|
56
97
|
|
|
98
|
+
function getActiveLocale(c) {
|
|
99
|
+
try {
|
|
100
|
+
const snap = (typeof c?.locale?.getLocale === 'function' ? c.locale.getLocale() : null)
|
|
101
|
+
|| (typeof c?.locale?.getSnapshot === 'function' ? c.locale.getSnapshot() : null);
|
|
102
|
+
const cur = snap?.active || snap?.locale || '';
|
|
103
|
+
if (typeof cur === 'string' && cur.toLowerCase().startsWith('zh')) return 'zh';
|
|
104
|
+
} catch (err) {
|
|
105
|
+
void err;
|
|
106
|
+
}
|
|
107
|
+
return 'en';
|
|
108
|
+
}
|
|
109
|
+
|
|
57
110
|
function makeT(dict, fallback) {
|
|
58
111
|
return function t(key, vars) {
|
|
59
112
|
let val = (dict && dict[key]) || (fallback && fallback[key]) || key;
|
|
@@ -79,7 +132,15 @@ window.__ModuleLoader__.load({
|
|
|
79
132
|
})
|
|
80
133
|
);
|
|
81
134
|
}
|
|
82
|
-
|
|
135
|
+
|
|
136
|
+
let ChevronIcon = null;
|
|
137
|
+
try {
|
|
138
|
+
const primitives = require('@deepseek-ai/dsh-client-ui-primitives');
|
|
139
|
+
ChevronIcon = primitives && primitives.IconChevronDownOutline14;
|
|
140
|
+
} catch (err) {
|
|
141
|
+
void err;
|
|
142
|
+
}
|
|
143
|
+
const Chevron = ChevronIcon || FallbackChevron;
|
|
83
144
|
|
|
84
145
|
function ensureCss() {
|
|
85
146
|
if (typeof document === 'undefined') return;
|
|
@@ -94,9 +155,9 @@ window.__ModuleLoader__.load({
|
|
|
94
155
|
.ug-head-sub{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:2px}
|
|
95
156
|
|
|
96
157
|
.ug-badge{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500}
|
|
97
|
-
.ug-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:
|
|
98
|
-
.ug-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:
|
|
99
|
-
.ug-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:
|
|
158
|
+
.ug-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-state-success-bg, var(--dsw-alias-bg-layer-2))}
|
|
159
|
+
.ug-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:var(--dsw-alias-state-warning-bg, var(--dsw-alias-bg-layer-2))}
|
|
160
|
+
.ug-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-state-error-bg, var(--dsw-alias-bg-layer-2))}
|
|
100
161
|
|
|
101
162
|
.ug-body{display:flex;flex-direction:column;gap:14px;border-top:1px solid var(--dsw-alias-border-l2);padding-top:16px;margin-top:4px}
|
|
102
163
|
.ug-field-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:6px}
|
|
@@ -104,21 +165,29 @@ window.__ModuleLoader__.load({
|
|
|
104
165
|
.ug-row-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}
|
|
105
166
|
.ug-field-desc{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.4}
|
|
106
167
|
|
|
107
|
-
.ug-checkbox{width:18px;height:18px;accent-color:var(--dsw-alias-state-brand-primary
|
|
168
|
+
.ug-checkbox{width:18px;height:18px;accent-color:var(--dsw-alias-state-brand-primary);cursor:pointer}
|
|
169
|
+
.ug-number-input{background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:6px 10px;color:var(--dsw-alias-label-primary);font-size:13px;font-family:inherit;width:140px}
|
|
170
|
+
.ug-number-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}
|
|
108
171
|
|
|
109
|
-
.ug-stats-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(
|
|
172
|
+
.ug-stats-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(180px, 1fr));gap:10px}
|
|
110
173
|
.ug-stat-box{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:2px}
|
|
111
174
|
.ug-stat-val{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}
|
|
175
|
+
.ug-stat-num{font-size:18px;font-weight:700;color:var(--dsw-alias-label-primary);letter-spacing:-0.02em}
|
|
112
176
|
.ug-stat-lbl{font-size:11px;color:var(--dsw-alias-label-secondary)}
|
|
113
177
|
|
|
114
178
|
.ug-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 16px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}
|
|
115
179
|
.ug-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}
|
|
180
|
+
.ug-btn-sm{padding:4px 10px;font-size:12px;border-radius:6px}
|
|
116
181
|
.ug-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
|
|
117
182
|
.ug-btn-primary:hover:not(:disabled){opacity:0.9}
|
|
183
|
+
.ug-btn-accent{background:var(--dsw-alias-state-brand-primary);color:var(--dsw-alias-label-invert, var(--dsw-alias-bg-layer-3));border-color:transparent}
|
|
184
|
+
.ug-btn-accent:hover:not(:disabled){opacity:0.9}
|
|
118
185
|
.ug-btn:disabled{opacity:0.5;cursor:not-allowed}
|
|
119
186
|
|
|
120
|
-
.ug-alert-ok{padding:10px 14px;border-radius:8px;background:
|
|
121
|
-
.ug-alert-bad{padding:10px 14px;border-radius:8px;background:
|
|
187
|
+
.ug-alert-ok{padding:10px 14px;border-radius:8px;background:var(--dsw-alias-state-success-bg, var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-success-primary);font-size:13px;border:1px solid var(--dsw-alias-state-success-border, var(--dsw-alias-border-l2))}
|
|
188
|
+
.ug-alert-bad{padding:10px 14px;border-radius:8px;background:var(--dsw-alias-state-error-bg, var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-error-primary);font-size:13px;border:1px solid var(--dsw-alias-state-error-border, var(--dsw-alias-border-l2))}
|
|
189
|
+
.ug-update-banner{margin-top:8px;display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-radius:6px;background:var(--dsw-alias-state-warning-bg, var(--dsw-alias-bg-layer-2));border:1px solid var(--dsw-alias-state-warning-border, var(--dsw-alias-border-l2))}
|
|
190
|
+
.ug-incident-box{margin-top:6px;padding:8px 10px;border-radius:6px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);font-size:12px;font-family:monospace;color:var(--dsw-alias-label-secondary);overflow-x:auto}
|
|
122
191
|
`;
|
|
123
192
|
document.head.appendChild(style);
|
|
124
193
|
}
|
|
@@ -149,8 +218,8 @@ window.__ModuleLoader__.load({
|
|
|
149
218
|
'button',
|
|
150
219
|
{
|
|
151
220
|
type: 'button',
|
|
152
|
-
className: 'ug-btn',
|
|
153
|
-
style: { marginTop: '8px'
|
|
221
|
+
className: 'ug-btn ug-btn-sm',
|
|
222
|
+
style: { marginTop: '8px' },
|
|
154
223
|
onClick: () => this.setState({ hasError: false, error: null }),
|
|
155
224
|
},
|
|
156
225
|
'Retry'
|
|
@@ -163,9 +232,222 @@ window.__ModuleLoader__.load({
|
|
|
163
232
|
}
|
|
164
233
|
const ErrorBoundary = createErrorBoundary();
|
|
165
234
|
|
|
235
|
+
function TelemetrySection({ t }) {
|
|
236
|
+
const [telemetry, setTelemetry] = React.useState(null);
|
|
237
|
+
const [loading, setLoading] = React.useState(false);
|
|
238
|
+
const [err, setErr] = React.useState(null);
|
|
239
|
+
|
|
240
|
+
const fetchTelemetry = React.useCallback(async () => {
|
|
241
|
+
setLoading(true);
|
|
242
|
+
setErr(null);
|
|
243
|
+
try {
|
|
244
|
+
const res = await fetch('/api/dsh-usage-guard/telemetry', {
|
|
245
|
+
headers: { accept: 'application/json' },
|
|
246
|
+
cache: 'no-store',
|
|
247
|
+
});
|
|
248
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
249
|
+
const data = await res.json();
|
|
250
|
+
setTelemetry(data);
|
|
251
|
+
} catch (e) {
|
|
252
|
+
setErr(e?.message || String(e));
|
|
253
|
+
} finally {
|
|
254
|
+
setLoading(false);
|
|
255
|
+
}
|
|
256
|
+
}, []);
|
|
257
|
+
|
|
258
|
+
React.useEffect(() => {
|
|
259
|
+
fetchTelemetry();
|
|
260
|
+
}, [fetchTelemetry]);
|
|
261
|
+
|
|
262
|
+
const recent = telemetry?.recentIncidents || [];
|
|
263
|
+
const lastIncident = recent.length > 0 ? recent[recent.length - 1] : null;
|
|
264
|
+
|
|
265
|
+
return React.createElement(
|
|
266
|
+
'div',
|
|
267
|
+
{ className: 'ug-field-card' },
|
|
268
|
+
React.createElement(
|
|
269
|
+
'div',
|
|
270
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
|
|
271
|
+
React.createElement('span', { className: 'ug-row-title' }, t('telemetry_title')),
|
|
272
|
+
React.createElement(
|
|
273
|
+
'button',
|
|
274
|
+
{
|
|
275
|
+
type: 'button',
|
|
276
|
+
className: 'ug-btn ug-btn-sm',
|
|
277
|
+
onClick: fetchTelemetry,
|
|
278
|
+
disabled: loading,
|
|
279
|
+
},
|
|
280
|
+
loading ? t('telemetry_refreshing') : t('telemetry_refresh')
|
|
281
|
+
)
|
|
282
|
+
),
|
|
283
|
+
err
|
|
284
|
+
? React.createElement('div', { className: 'ug-alert-bad', style: { padding: '6px 10px', fontSize: '12px' } }, err)
|
|
285
|
+
: null,
|
|
286
|
+
React.createElement(
|
|
287
|
+
'div',
|
|
288
|
+
{ className: 'ug-stats-grid', style: { marginTop: '8px' } },
|
|
289
|
+
React.createElement(
|
|
290
|
+
'div',
|
|
291
|
+
{ className: 'ug-stat-box' },
|
|
292
|
+
React.createElement('span', { className: 'ug-stat-lbl' }, t('telemetry_rescued')),
|
|
293
|
+
React.createElement('span', { className: 'ug-stat-num', style: { color: 'var(--dsw-alias-state-success-primary)' } }, String(telemetry?.rescuedEvents ?? 0))
|
|
294
|
+
),
|
|
295
|
+
React.createElement(
|
|
296
|
+
'div',
|
|
297
|
+
{ className: 'ug-stat-box' },
|
|
298
|
+
React.createElement('span', { className: 'ug-stat-lbl' }, t('telemetry_tokens')),
|
|
299
|
+
React.createElement('span', { className: 'ug-stat-num', style: { color: 'var(--dsw-alias-state-brand-primary)' } }, Number(telemetry?.fixedTokens ?? 0).toLocaleString())
|
|
300
|
+
),
|
|
301
|
+
React.createElement(
|
|
302
|
+
'div',
|
|
303
|
+
{ className: 'ug-stat-box' },
|
|
304
|
+
React.createElement('span', { className: 'ug-stat-lbl' }, t('telemetry_clamped')),
|
|
305
|
+
React.createElement('span', { className: 'ug-stat-num', style: { color: 'var(--dsw-alias-state-warning-primary)' } }, String(telemetry?.clampedSpikes ?? 0))
|
|
306
|
+
)
|
|
307
|
+
),
|
|
308
|
+
React.createElement(
|
|
309
|
+
'div',
|
|
310
|
+
{ style: { marginTop: '8px' } },
|
|
311
|
+
React.createElement('div', { className: 'ug-stat-lbl' }, t('telemetry_last_incident')),
|
|
312
|
+
lastIncident
|
|
313
|
+
? React.createElement(
|
|
314
|
+
'div',
|
|
315
|
+
{ className: 'ug-incident-box' },
|
|
316
|
+
`[${lastIncident.timestamp}] ${lastIncident.kind || 'incident'} | Turn: ${lastIncident.turn ?? '-'} Step: ${lastIncident.step ?? '-'} | Target: ${lastIncident.target || '-'}`
|
|
317
|
+
)
|
|
318
|
+
: React.createElement(
|
|
319
|
+
'div',
|
|
320
|
+
{ style: { fontSize: '12px', color: 'var(--dsw-alias-state-success-primary)', marginTop: '4px' } },
|
|
321
|
+
`✓ ${t('telemetry_no_incidents')}`
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function UpdaterSection({ t }) {
|
|
328
|
+
const [status, setStatus] = React.useState(null);
|
|
329
|
+
const [loading, setLoading] = React.useState(false);
|
|
330
|
+
const [updating, setUpdating] = React.useState(false);
|
|
331
|
+
const [feedback, setFeedback] = React.useState({ text: '', ok: true });
|
|
332
|
+
|
|
333
|
+
const checkUpdate = React.useCallback(async () => {
|
|
334
|
+
setLoading(true);
|
|
335
|
+
setFeedback({ text: '', ok: true });
|
|
336
|
+
try {
|
|
337
|
+
const res = await fetch('/api/dsh-usage-guard/update', {
|
|
338
|
+
headers: { accept: 'application/json' },
|
|
339
|
+
cache: 'no-store',
|
|
340
|
+
});
|
|
341
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
342
|
+
const data = await res.json();
|
|
343
|
+
setStatus(data);
|
|
344
|
+
} catch (e) {
|
|
345
|
+
setFeedback({ text: `${t('updater_failed')}: ${e?.message || String(e)}`, ok: false });
|
|
346
|
+
} finally {
|
|
347
|
+
setLoading(false);
|
|
348
|
+
}
|
|
349
|
+
}, [t]);
|
|
350
|
+
|
|
351
|
+
const onUpdateNow = async () => {
|
|
352
|
+
setUpdating(true);
|
|
353
|
+
setFeedback({ text: '', ok: true });
|
|
354
|
+
try {
|
|
355
|
+
const res = await fetch('/api/dsh-usage-guard/update', {
|
|
356
|
+
method: 'POST',
|
|
357
|
+
headers: {
|
|
358
|
+
'x-dsh-plugin-update': '1',
|
|
359
|
+
'content-type': 'application/json',
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
const data = await res.json();
|
|
363
|
+
if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
|
|
364
|
+
setStatus(data);
|
|
365
|
+
setFeedback({ text: t('updater_success'), ok: true });
|
|
366
|
+
} catch (e) {
|
|
367
|
+
setFeedback({ text: e?.message || String(e), ok: false });
|
|
368
|
+
} finally {
|
|
369
|
+
setUpdating(false);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
React.useEffect(() => {
|
|
374
|
+
checkUpdate();
|
|
375
|
+
}, [checkUpdate]);
|
|
376
|
+
|
|
377
|
+
const currentVer = status?.currentVersion || '0.1.9';
|
|
378
|
+
const latestVer = status?.latestVersion;
|
|
379
|
+
const updateAvailable = !!status?.updateAvailable && !!latestVer && latestVer !== currentVer;
|
|
380
|
+
|
|
381
|
+
return React.createElement(
|
|
382
|
+
'div',
|
|
383
|
+
{ className: 'ug-field-card' },
|
|
384
|
+
React.createElement(
|
|
385
|
+
'div',
|
|
386
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
|
|
387
|
+
React.createElement('span', { className: 'ug-row-title' }, t('updater_title')),
|
|
388
|
+
React.createElement(
|
|
389
|
+
'button',
|
|
390
|
+
{
|
|
391
|
+
type: 'button',
|
|
392
|
+
className: 'ug-btn ug-btn-sm',
|
|
393
|
+
onClick: checkUpdate,
|
|
394
|
+
disabled: loading || updating,
|
|
395
|
+
},
|
|
396
|
+
loading ? t('updater_checking') : t('updater_check')
|
|
397
|
+
)
|
|
398
|
+
),
|
|
399
|
+
React.createElement(
|
|
400
|
+
'div',
|
|
401
|
+
{ style: { display: 'flex', alignItems: 'center', gap: '16px', marginTop: '6px', fontSize: '13px' } },
|
|
402
|
+
React.createElement(
|
|
403
|
+
'div',
|
|
404
|
+
null,
|
|
405
|
+
React.createElement('span', { className: 'ug-stat-lbl' }, t('updater_current') + ': '),
|
|
406
|
+
React.createElement('strong', { style: { color: 'var(--dsw-alias-label-primary)' } }, `v${currentVer}`)
|
|
407
|
+
),
|
|
408
|
+
latestVer
|
|
409
|
+
? React.createElement(
|
|
410
|
+
'div',
|
|
411
|
+
null,
|
|
412
|
+
React.createElement('span', { className: 'ug-stat-lbl' }, t('updater_latest') + ': '),
|
|
413
|
+
React.createElement(
|
|
414
|
+
'strong',
|
|
415
|
+
{ style: { color: updateAvailable ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-state-success-primary)' } },
|
|
416
|
+
`v${latestVer}`
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
: null
|
|
420
|
+
),
|
|
421
|
+
updateAvailable
|
|
422
|
+
? React.createElement(
|
|
423
|
+
'div',
|
|
424
|
+
{ className: 'ug-update-banner' },
|
|
425
|
+
React.createElement('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-state-warning-primary)', fontWeight: 500 } }, `New release available: v${latestVer}`),
|
|
426
|
+
React.createElement(
|
|
427
|
+
'button',
|
|
428
|
+
{
|
|
429
|
+
type: 'button',
|
|
430
|
+
className: 'ug-btn ug-btn-sm ug-btn-accent',
|
|
431
|
+
onClick: onUpdateNow,
|
|
432
|
+
disabled: updating,
|
|
433
|
+
},
|
|
434
|
+
updating ? t('updater_updating') : t('updater_update_now', { version: latestVer })
|
|
435
|
+
)
|
|
436
|
+
)
|
|
437
|
+
: null,
|
|
438
|
+
feedback.text
|
|
439
|
+
? React.createElement(
|
|
440
|
+
'div',
|
|
441
|
+
{ className: feedback.ok ? 'ug-alert-ok' : 'ug-alert-bad', style: { marginTop: '8px', padding: '6px 10px', fontSize: '12px' } },
|
|
442
|
+
feedback.text
|
|
443
|
+
)
|
|
444
|
+
: null
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
166
448
|
function SettingsPage({ ctx, t }) {
|
|
167
449
|
const [snap, setSnap] = React.useState({ status: 'loading', value: {} });
|
|
168
|
-
const [draft, setDraft] = React.useState({ repair: true, report: true });
|
|
450
|
+
const [draft, setDraft] = React.useState({ repair: true, report: true, maxStepTokens: 1000000 });
|
|
169
451
|
const [saving, setSaving] = React.useState(false);
|
|
170
452
|
const [msg, setMsg] = React.useState({ text: '', ok: true });
|
|
171
453
|
const isDirtyRef = React.useRef(false);
|
|
@@ -190,6 +472,7 @@ window.__ModuleLoader__.load({
|
|
|
190
472
|
setDraft({
|
|
191
473
|
repair: res.repair !== false,
|
|
192
474
|
report: res.report !== false,
|
|
475
|
+
maxStepTokens: typeof res.maxStepTokens === 'number' ? res.maxStepTokens : 1000000,
|
|
193
476
|
});
|
|
194
477
|
}
|
|
195
478
|
}
|
|
@@ -198,6 +481,9 @@ window.__ModuleLoader__.load({
|
|
|
198
481
|
}
|
|
199
482
|
};
|
|
200
483
|
read();
|
|
484
|
+
if (typeof scope.watch !== 'function' && active) {
|
|
485
|
+
setSnap((prev) => ({ ...prev, unmonitored: true }));
|
|
486
|
+
}
|
|
201
487
|
const unwatch = typeof scope.watch === 'function'
|
|
202
488
|
? scope.watch((next) => {
|
|
203
489
|
if (!active || !next || typeof next !== 'object') return;
|
|
@@ -206,13 +492,16 @@ window.__ModuleLoader__.load({
|
|
|
206
492
|
setDraft({
|
|
207
493
|
repair: next.repair !== false,
|
|
208
494
|
report: next.report !== false,
|
|
495
|
+
maxStepTokens: typeof next.maxStepTokens === 'number' ? next.maxStepTokens : 1000000,
|
|
209
496
|
});
|
|
210
497
|
}
|
|
211
498
|
})
|
|
212
499
|
: () => {};
|
|
213
500
|
return () => {
|
|
214
501
|
active = false;
|
|
215
|
-
try { unwatch(); } catch (_) {
|
|
502
|
+
try { unwatch(); } catch (_) {
|
|
503
|
+
// Intentional cleanup catch: ignore errors if subscription was already disposed
|
|
504
|
+
}
|
|
216
505
|
};
|
|
217
506
|
} catch (_) {
|
|
218
507
|
setSnap({ status: 'unavailable', value: {} });
|
|
@@ -243,13 +532,23 @@ window.__ModuleLoader__.load({
|
|
|
243
532
|
setDraft((prev) => ({ ...prev, [field]: checked }));
|
|
244
533
|
};
|
|
245
534
|
|
|
535
|
+
const handleNumberChange = (field, rawVal) => {
|
|
536
|
+
isDirtyRef.current = true;
|
|
537
|
+
if (msg.text) {
|
|
538
|
+
if (msgTimerRef.current) clearTimeout(msgTimerRef.current);
|
|
539
|
+
setMsg({ text: '', ok: true });
|
|
540
|
+
}
|
|
541
|
+
const val = rawVal === '' ? 0 : Number(rawVal);
|
|
542
|
+
setDraft((prev) => ({ ...prev, [field]: isNaN(val) ? 0 : val }));
|
|
543
|
+
};
|
|
544
|
+
|
|
246
545
|
const onSave = async () => {
|
|
247
546
|
if (!ctx?.settingsScope || typeof ctx.settingsScope.bind !== 'function') return;
|
|
248
547
|
setSaving(true);
|
|
249
548
|
try {
|
|
250
549
|
const scope = ctx.settingsScope.bind({ namespace: NS });
|
|
251
550
|
const errs = [];
|
|
252
|
-
for (const k of ['repair', 'report']) {
|
|
551
|
+
for (const k of ['repair', 'report', 'maxStepTokens']) {
|
|
253
552
|
try {
|
|
254
553
|
await scope.set(k, draft[k]);
|
|
255
554
|
} catch (e) {
|
|
@@ -290,6 +589,14 @@ window.__ModuleLoader__.load({
|
|
|
290
589
|
'div',
|
|
291
590
|
{ className: 'ug-body' },
|
|
292
591
|
|
|
592
|
+
snap.unmonitored
|
|
593
|
+
? React.createElement(
|
|
594
|
+
'div',
|
|
595
|
+
{ className: 'ug-alert-bad', style: { marginBottom: '12px' } },
|
|
596
|
+
t('settings.unmonitored')
|
|
597
|
+
)
|
|
598
|
+
: null,
|
|
599
|
+
|
|
293
600
|
// Telemetry / Protection status banner
|
|
294
601
|
React.createElement(
|
|
295
602
|
'div',
|
|
@@ -353,6 +660,27 @@ window.__ModuleLoader__.load({
|
|
|
353
660
|
React.createElement('div', { className: 'ug-field-desc' }, t('reportDesc'))
|
|
354
661
|
),
|
|
355
662
|
|
|
663
|
+
// Field 3: maxStepTokens
|
|
664
|
+
React.createElement(
|
|
665
|
+
'div',
|
|
666
|
+
{ className: 'ug-field-card' },
|
|
667
|
+
React.createElement(
|
|
668
|
+
'div',
|
|
669
|
+
{ className: 'ug-row' },
|
|
670
|
+
React.createElement('label', { className: 'ug-row-title', htmlFor: 'ug-tokens-input' }, t('maxStepTokens')),
|
|
671
|
+
React.createElement('input', {
|
|
672
|
+
id: 'ug-tokens-input',
|
|
673
|
+
type: 'number',
|
|
674
|
+
className: 'ug-number-input',
|
|
675
|
+
min: 0,
|
|
676
|
+
step: 10000,
|
|
677
|
+
value: draft.maxStepTokens ?? 1000000,
|
|
678
|
+
onChange: (e) => handleNumberChange('maxStepTokens', e.target.value),
|
|
679
|
+
})
|
|
680
|
+
),
|
|
681
|
+
React.createElement('div', { className: 'ug-field-desc' }, t('maxStepTokensDesc'))
|
|
682
|
+
),
|
|
683
|
+
|
|
356
684
|
// Feedback message
|
|
357
685
|
msg.text
|
|
358
686
|
? React.createElement(
|
|
@@ -362,7 +690,7 @@ window.__ModuleLoader__.load({
|
|
|
362
690
|
)
|
|
363
691
|
: null,
|
|
364
692
|
|
|
365
|
-
//
|
|
693
|
+
// Save action
|
|
366
694
|
React.createElement(
|
|
367
695
|
'div',
|
|
368
696
|
{ style: { display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '4px' } },
|
|
@@ -376,14 +704,23 @@ window.__ModuleLoader__.load({
|
|
|
376
704
|
},
|
|
377
705
|
saving ? t('saving') : t('save')
|
|
378
706
|
)
|
|
379
|
-
)
|
|
707
|
+
),
|
|
708
|
+
|
|
709
|
+
// Live Telemetry section
|
|
710
|
+
React.createElement(TelemetrySection, { t }),
|
|
711
|
+
|
|
712
|
+
// Updater section
|
|
713
|
+
React.createElement(UpdaterSection, { t })
|
|
380
714
|
);
|
|
381
715
|
}
|
|
382
716
|
|
|
383
717
|
// Accordion PluginCard for settings.plugin.item
|
|
384
718
|
function PluginCard(props) {
|
|
385
719
|
const [open, setOpen] = React.useState(false);
|
|
386
|
-
const
|
|
720
|
+
const hostCtx = (props && props.ctx) || ctx;
|
|
721
|
+
const locale = getActiveLocale(hostCtx);
|
|
722
|
+
const dict = locale === 'zh' ? zh : en;
|
|
723
|
+
const t = props?.t || makeT(dict, en);
|
|
387
724
|
|
|
388
725
|
React.useEffect(() => {
|
|
389
726
|
ensureCss();
|
|
@@ -430,7 +767,7 @@ window.__ModuleLoader__.load({
|
|
|
430
767
|
null,
|
|
431
768
|
React.createElement(SettingsPage, {
|
|
432
769
|
...props,
|
|
433
|
-
ctx:
|
|
770
|
+
ctx: hostCtx,
|
|
434
771
|
t,
|
|
435
772
|
})
|
|
436
773
|
)
|
|
@@ -438,10 +775,10 @@ window.__ModuleLoader__.load({
|
|
|
438
775
|
);
|
|
439
776
|
}
|
|
440
777
|
|
|
441
|
-
function refreshMirrorUntilVisible(
|
|
778
|
+
function refreshMirrorUntilVisible(c) {
|
|
442
779
|
const visible = () => {
|
|
443
780
|
try {
|
|
444
|
-
const s = (
|
|
781
|
+
const s = (c?.get && c.get('lanSettings')) || c?.settingsScope;
|
|
445
782
|
const view = s?.describe?.()?.getSnapshot?.()?.view;
|
|
446
783
|
return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS);
|
|
447
784
|
} catch (_) {
|
|
@@ -457,9 +794,13 @@ window.__ModuleLoader__.load({
|
|
|
457
794
|
}
|
|
458
795
|
tries += 1;
|
|
459
796
|
try {
|
|
460
|
-
const s = (
|
|
797
|
+
const s = (c?.get && c.get('lanSettings')) || c?.settingsScope;
|
|
461
798
|
s?.describe?.()?.load?.();
|
|
462
|
-
} catch (
|
|
799
|
+
} catch (err) {
|
|
800
|
+
if (typeof console !== 'undefined' && console.warn) {
|
|
801
|
+
console.warn('[dsh-usage-guard] settings mirror load attempt failed:', err?.message || err);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
463
804
|
}, 1000);
|
|
464
805
|
if (timer && typeof timer.unref === 'function') {
|
|
465
806
|
timer.unref();
|
|
@@ -482,14 +823,14 @@ window.__ModuleLoader__.load({
|
|
|
482
823
|
if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
483
824
|
if (typeof ctx.effect === 'function') {
|
|
484
825
|
ctx.effect(() => {
|
|
485
|
-
const undo = [addLocale('en', en), addLocale('
|
|
826
|
+
const undo = [addLocale('en', en), addLocale('zh', zh)];
|
|
486
827
|
return () => {
|
|
487
828
|
for (const off of undo) off();
|
|
488
829
|
};
|
|
489
830
|
}, 'dsh-usage-guard: dictionaries');
|
|
490
831
|
} else {
|
|
491
832
|
addLocale('en', en);
|
|
492
|
-
addLocale('
|
|
833
|
+
addLocale('zh', zh);
|
|
493
834
|
}
|
|
494
835
|
}
|
|
495
836
|
|
|
@@ -549,6 +890,7 @@ window.__ModuleLoader__.load({
|
|
|
549
890
|
makeT,
|
|
550
891
|
ensureCss,
|
|
551
892
|
createErrorBoundary,
|
|
893
|
+
getActiveLocale,
|
|
552
894
|
};
|
|
553
895
|
return module.exports;
|
|
554
896
|
},
|