@goodandready/dsh-usage-guard 0.1.7 → 0.1.9
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 +347 -36
- package/lib/index.js +75 -41
- package/lib/telemetry.js +111 -0
- package/lib/updater.js +266 -0
- package/lib/usage.js +60 -43
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -22,38 +22,86 @@ 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.retry': '
|
|
42
|
-
saved: '
|
|
43
|
-
save: '
|
|
44
|
-
saving: '
|
|
45
|
-
repair: '
|
|
46
|
-
repairDesc: '
|
|
47
|
-
report: '
|
|
48
|
-
reportDesc: '
|
|
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.retry': '重试',
|
|
62
|
+
saved: '设置保存成功',
|
|
63
|
+
save: '保存更改',
|
|
64
|
+
saving: '正在保存…',
|
|
65
|
+
repair: '自动修复 Token 计数器',
|
|
66
|
+
repairDesc: '在求和前将缺失、非数值或格式错误的计数器替换为 0,防止 NaN 导致会话历史崩溃锁死。',
|
|
67
|
+
report: '记录诊断告警',
|
|
68
|
+
reportDesc: '每当收到损坏的用量数据时,记录详细警告并指出会话轮次、步骤和原始样本。',
|
|
69
|
+
maxStepTokens: '单步 Token 突增削峰阈值',
|
|
70
|
+
maxStepTokensDesc: '在进行投影计算前,对异常巨大的单步用量计数器(例如 > 1,000,000)进行削峰限制,防止整数溢出与会话汇总异常。设为 0 表示禁用。',
|
|
71
|
+
stats_title: '保护状态',
|
|
72
|
+
stats_mode: '当前模式',
|
|
73
|
+
stats_mode_repair: '主动修复(安全)',
|
|
74
|
+
stats_mode_report: '仅审计(不安全)',
|
|
75
|
+
stats_scope: '作用范围',
|
|
54
76
|
stats_scope_val: 'sessionProjections',
|
|
77
|
+
telemetry_title: '实时拦截指标',
|
|
78
|
+
telemetry_rescued: '已挽救异常事件',
|
|
79
|
+
telemetry_tokens: '已修复 Token 数',
|
|
80
|
+
telemetry_clamped: '削峰拦截次数',
|
|
81
|
+
telemetry_last_incident: '最近一次拦截事件',
|
|
82
|
+
telemetry_no_incidents: '未检测到拦截事件(系统运行健康)',
|
|
83
|
+
telemetry_refresh: '刷新指标',
|
|
84
|
+
telemetry_refreshing: '正在刷新…',
|
|
85
|
+
updater_title: '插件版本与在线更新',
|
|
86
|
+
updater_current: '当前版本',
|
|
87
|
+
updater_latest: '最新版本',
|
|
88
|
+
updater_checking: '正在检查…',
|
|
89
|
+
updater_check: '检查更新',
|
|
90
|
+
updater_updating: '正在更新…',
|
|
91
|
+
updater_update_now: '立即更新至 {version}',
|
|
92
|
+
updater_up_to_date: '已是最新版本',
|
|
93
|
+
updater_failed: '检查更新失败',
|
|
94
|
+
updater_success: '更新成功,请刷新页面。',
|
|
55
95
|
};
|
|
56
96
|
|
|
97
|
+
function getActiveLocale(c) {
|
|
98
|
+
try {
|
|
99
|
+
const cur = (c?.locale && typeof c.locale.get === 'function' ? c.locale.get() : c?.locale?.current) || '';
|
|
100
|
+
if (typeof cur === 'string' && cur.toLowerCase().startsWith('zh')) return 'zh';
|
|
101
|
+
} catch (_) {}
|
|
102
|
+
return 'en';
|
|
103
|
+
}
|
|
104
|
+
|
|
57
105
|
function makeT(dict, fallback) {
|
|
58
106
|
return function t(key, vars) {
|
|
59
107
|
let val = (dict && dict[key]) || (fallback && fallback[key]) || key;
|
|
@@ -104,21 +152,28 @@ window.__ModuleLoader__.load({
|
|
|
104
152
|
.ug-row-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}
|
|
105
153
|
.ug-field-desc{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.4}
|
|
106
154
|
|
|
107
|
-
.ug-checkbox{width:18px;height:18px;accent-color:var(--dsw-alias-state-brand-primary
|
|
155
|
+
.ug-checkbox{width:18px;height:18px;accent-color:var(--dsw-alias-state-brand-primary);cursor:pointer}
|
|
156
|
+
.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}
|
|
157
|
+
.ug-number-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}
|
|
108
158
|
|
|
109
|
-
.ug-stats-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(
|
|
159
|
+
.ug-stats-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(180px, 1fr));gap:10px}
|
|
110
160
|
.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
161
|
.ug-stat-val{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}
|
|
162
|
+
.ug-stat-num{font-size:18px;font-weight:700;color:var(--dsw-alias-label-primary);letter-spacing:-0.02em}
|
|
112
163
|
.ug-stat-lbl{font-size:11px;color:var(--dsw-alias-label-secondary)}
|
|
113
164
|
|
|
114
165
|
.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
166
|
.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))}
|
|
167
|
+
.ug-btn-sm{padding:4px 10px;font-size:12px;border-radius:6px}
|
|
116
168
|
.ug-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
|
|
117
169
|
.ug-btn-primary:hover:not(:disabled){opacity:0.9}
|
|
170
|
+
.ug-btn-accent{background:var(--dsw-alias-state-brand-primary);color:#ffffff;border-color:transparent}
|
|
171
|
+
.ug-btn-accent:hover:not(:disabled){opacity:0.9}
|
|
118
172
|
.ug-btn:disabled{opacity:0.5;cursor:not-allowed}
|
|
119
173
|
|
|
120
174
|
.ug-alert-ok{padding:10px 14px;border-radius:8px;background:rgba(16,185,129,0.1);color:var(--dsw-alias-state-success-primary);font-size:13px;border:1px solid rgba(16,185,129,0.2)}
|
|
121
175
|
.ug-alert-bad{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px;border:1px solid rgba(239,68,68,0.2)}
|
|
176
|
+
.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
177
|
`;
|
|
123
178
|
document.head.appendChild(style);
|
|
124
179
|
}
|
|
@@ -149,8 +204,8 @@ window.__ModuleLoader__.load({
|
|
|
149
204
|
'button',
|
|
150
205
|
{
|
|
151
206
|
type: 'button',
|
|
152
|
-
className: 'ug-btn',
|
|
153
|
-
style: { marginTop: '8px'
|
|
207
|
+
className: 'ug-btn ug-btn-sm',
|
|
208
|
+
style: { marginTop: '8px' },
|
|
154
209
|
onClick: () => this.setState({ hasError: false, error: null }),
|
|
155
210
|
},
|
|
156
211
|
'Retry'
|
|
@@ -163,9 +218,222 @@ window.__ModuleLoader__.load({
|
|
|
163
218
|
}
|
|
164
219
|
const ErrorBoundary = createErrorBoundary();
|
|
165
220
|
|
|
221
|
+
function TelemetrySection({ t }) {
|
|
222
|
+
const [telemetry, setTelemetry] = React.useState(null);
|
|
223
|
+
const [loading, setLoading] = React.useState(false);
|
|
224
|
+
const [err, setErr] = React.useState(null);
|
|
225
|
+
|
|
226
|
+
const fetchTelemetry = React.useCallback(async () => {
|
|
227
|
+
setLoading(true);
|
|
228
|
+
setErr(null);
|
|
229
|
+
try {
|
|
230
|
+
const res = await fetch('/api/dsh-usage-guard/telemetry', {
|
|
231
|
+
headers: { accept: 'application/json' },
|
|
232
|
+
cache: 'no-store',
|
|
233
|
+
});
|
|
234
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
235
|
+
const data = await res.json();
|
|
236
|
+
setTelemetry(data);
|
|
237
|
+
} catch (e) {
|
|
238
|
+
setErr(e?.message || String(e));
|
|
239
|
+
} finally {
|
|
240
|
+
setLoading(false);
|
|
241
|
+
}
|
|
242
|
+
}, []);
|
|
243
|
+
|
|
244
|
+
React.useEffect(() => {
|
|
245
|
+
fetchTelemetry();
|
|
246
|
+
}, [fetchTelemetry]);
|
|
247
|
+
|
|
248
|
+
const recent = telemetry?.recentIncidents || [];
|
|
249
|
+
const lastIncident = recent.length > 0 ? recent[recent.length - 1] : null;
|
|
250
|
+
|
|
251
|
+
return React.createElement(
|
|
252
|
+
'div',
|
|
253
|
+
{ className: 'ug-field-card' },
|
|
254
|
+
React.createElement(
|
|
255
|
+
'div',
|
|
256
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
|
|
257
|
+
React.createElement('span', { className: 'ug-row-title' }, t('telemetry_title')),
|
|
258
|
+
React.createElement(
|
|
259
|
+
'button',
|
|
260
|
+
{
|
|
261
|
+
type: 'button',
|
|
262
|
+
className: 'ug-btn ug-btn-sm',
|
|
263
|
+
onClick: fetchTelemetry,
|
|
264
|
+
disabled: loading,
|
|
265
|
+
},
|
|
266
|
+
loading ? t('telemetry_refreshing') : t('telemetry_refresh')
|
|
267
|
+
)
|
|
268
|
+
),
|
|
269
|
+
err
|
|
270
|
+
? React.createElement('div', { className: 'ug-alert-bad', style: { padding: '6px 10px', fontSize: '12px' } }, err)
|
|
271
|
+
: null,
|
|
272
|
+
React.createElement(
|
|
273
|
+
'div',
|
|
274
|
+
{ className: 'ug-stats-grid', style: { marginTop: '8px' } },
|
|
275
|
+
React.createElement(
|
|
276
|
+
'div',
|
|
277
|
+
{ className: 'ug-stat-box' },
|
|
278
|
+
React.createElement('span', { className: 'ug-stat-lbl' }, t('telemetry_rescued')),
|
|
279
|
+
React.createElement('span', { className: 'ug-stat-num', style: { color: 'var(--dsw-alias-state-success-primary)' } }, String(telemetry?.rescuedEvents ?? 0))
|
|
280
|
+
),
|
|
281
|
+
React.createElement(
|
|
282
|
+
'div',
|
|
283
|
+
{ className: 'ug-stat-box' },
|
|
284
|
+
React.createElement('span', { className: 'ug-stat-lbl' }, t('telemetry_tokens')),
|
|
285
|
+
React.createElement('span', { className: 'ug-stat-num', style: { color: 'var(--dsw-alias-state-brand-primary)' } }, Number(telemetry?.fixedTokens ?? 0).toLocaleString())
|
|
286
|
+
),
|
|
287
|
+
React.createElement(
|
|
288
|
+
'div',
|
|
289
|
+
{ className: 'ug-stat-box' },
|
|
290
|
+
React.createElement('span', { className: 'ug-stat-lbl' }, t('telemetry_clamped')),
|
|
291
|
+
React.createElement('span', { className: 'ug-stat-num', style: { color: 'var(--dsw-alias-state-warning-primary)' } }, String(telemetry?.clampedSpikes ?? 0))
|
|
292
|
+
)
|
|
293
|
+
),
|
|
294
|
+
React.createElement(
|
|
295
|
+
'div',
|
|
296
|
+
{ style: { marginTop: '8px' } },
|
|
297
|
+
React.createElement('div', { className: 'ug-stat-lbl' }, t('telemetry_last_incident')),
|
|
298
|
+
lastIncident
|
|
299
|
+
? React.createElement(
|
|
300
|
+
'div',
|
|
301
|
+
{ className: 'ug-incident-box' },
|
|
302
|
+
`[${lastIncident.timestamp}] ${lastIncident.kind || 'incident'} | Turn: ${lastIncident.turn ?? '-'} Step: ${lastIncident.step ?? '-'} | Target: ${lastIncident.target || '-'}`
|
|
303
|
+
)
|
|
304
|
+
: React.createElement(
|
|
305
|
+
'div',
|
|
306
|
+
{ style: { fontSize: '12px', color: 'var(--dsw-alias-state-success-primary)', marginTop: '4px' } },
|
|
307
|
+
`✓ ${t('telemetry_no_incidents')}`
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function UpdaterSection({ t }) {
|
|
314
|
+
const [status, setStatus] = React.useState(null);
|
|
315
|
+
const [loading, setLoading] = React.useState(false);
|
|
316
|
+
const [updating, setUpdating] = React.useState(false);
|
|
317
|
+
const [feedback, setFeedback] = React.useState({ text: '', ok: true });
|
|
318
|
+
|
|
319
|
+
const checkUpdate = React.useCallback(async () => {
|
|
320
|
+
setLoading(true);
|
|
321
|
+
setFeedback({ text: '', ok: true });
|
|
322
|
+
try {
|
|
323
|
+
const res = await fetch('/api/dsh-usage-guard/update', {
|
|
324
|
+
headers: { accept: 'application/json' },
|
|
325
|
+
cache: 'no-store',
|
|
326
|
+
});
|
|
327
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
328
|
+
const data = await res.json();
|
|
329
|
+
setStatus(data);
|
|
330
|
+
} catch (e) {
|
|
331
|
+
setFeedback({ text: `${t('updater_failed')}: ${e?.message || String(e)}`, ok: false });
|
|
332
|
+
} finally {
|
|
333
|
+
setLoading(false);
|
|
334
|
+
}
|
|
335
|
+
}, [t]);
|
|
336
|
+
|
|
337
|
+
const onUpdateNow = async () => {
|
|
338
|
+
setUpdating(true);
|
|
339
|
+
setFeedback({ text: '', ok: true });
|
|
340
|
+
try {
|
|
341
|
+
const res = await fetch('/api/dsh-usage-guard/update', {
|
|
342
|
+
method: 'POST',
|
|
343
|
+
headers: {
|
|
344
|
+
'x-dsh-plugin-update': '1',
|
|
345
|
+
'content-type': 'application/json',
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
const data = await res.json();
|
|
349
|
+
if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
|
|
350
|
+
setStatus(data);
|
|
351
|
+
setFeedback({ text: t('updater_success'), ok: true });
|
|
352
|
+
} catch (e) {
|
|
353
|
+
setFeedback({ text: e?.message || String(e), ok: false });
|
|
354
|
+
} finally {
|
|
355
|
+
setUpdating(false);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
React.useEffect(() => {
|
|
360
|
+
checkUpdate();
|
|
361
|
+
}, [checkUpdate]);
|
|
362
|
+
|
|
363
|
+
const currentVer = status?.currentVersion || '0.1.9';
|
|
364
|
+
const latestVer = status?.latestVersion;
|
|
365
|
+
const updateAvailable = !!status?.updateAvailable && !!latestVer && latestVer !== currentVer;
|
|
366
|
+
|
|
367
|
+
return React.createElement(
|
|
368
|
+
'div',
|
|
369
|
+
{ className: 'ug-field-card' },
|
|
370
|
+
React.createElement(
|
|
371
|
+
'div',
|
|
372
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
|
|
373
|
+
React.createElement('span', { className: 'ug-row-title' }, t('updater_title')),
|
|
374
|
+
React.createElement(
|
|
375
|
+
'button',
|
|
376
|
+
{
|
|
377
|
+
type: 'button',
|
|
378
|
+
className: 'ug-btn ug-btn-sm',
|
|
379
|
+
onClick: checkUpdate,
|
|
380
|
+
disabled: loading || updating,
|
|
381
|
+
},
|
|
382
|
+
loading ? t('updater_checking') : t('updater_check')
|
|
383
|
+
)
|
|
384
|
+
),
|
|
385
|
+
React.createElement(
|
|
386
|
+
'div',
|
|
387
|
+
{ style: { display: 'flex', alignItems: 'center', gap: '16px', marginTop: '6px', fontSize: '13px' } },
|
|
388
|
+
React.createElement(
|
|
389
|
+
'div',
|
|
390
|
+
null,
|
|
391
|
+
React.createElement('span', { className: 'ug-stat-lbl' }, t('updater_current') + ': '),
|
|
392
|
+
React.createElement('strong', { style: { color: 'var(--dsw-alias-label-primary)' } }, `v${currentVer}`)
|
|
393
|
+
),
|
|
394
|
+
latestVer
|
|
395
|
+
? React.createElement(
|
|
396
|
+
'div',
|
|
397
|
+
null,
|
|
398
|
+
React.createElement('span', { className: 'ug-stat-lbl' }, t('updater_latest') + ': '),
|
|
399
|
+
React.createElement(
|
|
400
|
+
'strong',
|
|
401
|
+
{ style: { color: updateAvailable ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-state-success-primary)' } },
|
|
402
|
+
`v${latestVer}`
|
|
403
|
+
)
|
|
404
|
+
)
|
|
405
|
+
: null
|
|
406
|
+
),
|
|
407
|
+
updateAvailable
|
|
408
|
+
? React.createElement(
|
|
409
|
+
'div',
|
|
410
|
+
{ style: { marginTop: '8px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 12px', background: 'rgba(245,158,11,0.08)', borderRadius: '6px', border: '1px solid rgba(245,158,11,0.2)' } },
|
|
411
|
+
React.createElement('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-state-warning-primary)', fontWeight: 500 } }, `New release available: v${latestVer}`),
|
|
412
|
+
React.createElement(
|
|
413
|
+
'button',
|
|
414
|
+
{
|
|
415
|
+
type: 'button',
|
|
416
|
+
className: 'ug-btn ug-btn-sm ug-btn-accent',
|
|
417
|
+
onClick: onUpdateNow,
|
|
418
|
+
disabled: updating,
|
|
419
|
+
},
|
|
420
|
+
updating ? t('updater_updating') : t('updater_update_now', { version: latestVer })
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
: null,
|
|
424
|
+
feedback.text
|
|
425
|
+
? React.createElement(
|
|
426
|
+
'div',
|
|
427
|
+
{ className: feedback.ok ? 'ug-alert-ok' : 'ug-alert-bad', style: { marginTop: '8px', padding: '6px 10px', fontSize: '12px' } },
|
|
428
|
+
feedback.text
|
|
429
|
+
)
|
|
430
|
+
: null
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
166
434
|
function SettingsPage({ ctx, t }) {
|
|
167
435
|
const [snap, setSnap] = React.useState({ status: 'loading', value: {} });
|
|
168
|
-
const [draft, setDraft] = React.useState({ repair: true, report: true });
|
|
436
|
+
const [draft, setDraft] = React.useState({ repair: true, report: true, maxStepTokens: 1000000 });
|
|
169
437
|
const [saving, setSaving] = React.useState(false);
|
|
170
438
|
const [msg, setMsg] = React.useState({ text: '', ok: true });
|
|
171
439
|
const isDirtyRef = React.useRef(false);
|
|
@@ -190,6 +458,7 @@ window.__ModuleLoader__.load({
|
|
|
190
458
|
setDraft({
|
|
191
459
|
repair: res.repair !== false,
|
|
192
460
|
report: res.report !== false,
|
|
461
|
+
maxStepTokens: typeof res.maxStepTokens === 'number' ? res.maxStepTokens : 1000000,
|
|
193
462
|
});
|
|
194
463
|
}
|
|
195
464
|
}
|
|
@@ -206,6 +475,7 @@ window.__ModuleLoader__.load({
|
|
|
206
475
|
setDraft({
|
|
207
476
|
repair: next.repair !== false,
|
|
208
477
|
report: next.report !== false,
|
|
478
|
+
maxStepTokens: typeof next.maxStepTokens === 'number' ? next.maxStepTokens : 1000000,
|
|
209
479
|
});
|
|
210
480
|
}
|
|
211
481
|
})
|
|
@@ -243,13 +513,23 @@ window.__ModuleLoader__.load({
|
|
|
243
513
|
setDraft((prev) => ({ ...prev, [field]: checked }));
|
|
244
514
|
};
|
|
245
515
|
|
|
516
|
+
const handleNumberChange = (field, rawVal) => {
|
|
517
|
+
isDirtyRef.current = true;
|
|
518
|
+
if (msg.text) {
|
|
519
|
+
if (msgTimerRef.current) clearTimeout(msgTimerRef.current);
|
|
520
|
+
setMsg({ text: '', ok: true });
|
|
521
|
+
}
|
|
522
|
+
const val = rawVal === '' ? 0 : Number(rawVal);
|
|
523
|
+
setDraft((prev) => ({ ...prev, [field]: isNaN(val) ? 0 : val }));
|
|
524
|
+
};
|
|
525
|
+
|
|
246
526
|
const onSave = async () => {
|
|
247
527
|
if (!ctx?.settingsScope || typeof ctx.settingsScope.bind !== 'function') return;
|
|
248
528
|
setSaving(true);
|
|
249
529
|
try {
|
|
250
530
|
const scope = ctx.settingsScope.bind({ namespace: NS });
|
|
251
531
|
const errs = [];
|
|
252
|
-
for (const k of ['repair', 'report']) {
|
|
532
|
+
for (const k of ['repair', 'report', 'maxStepTokens']) {
|
|
253
533
|
try {
|
|
254
534
|
await scope.set(k, draft[k]);
|
|
255
535
|
} catch (e) {
|
|
@@ -353,6 +633,27 @@ window.__ModuleLoader__.load({
|
|
|
353
633
|
React.createElement('div', { className: 'ug-field-desc' }, t('reportDesc'))
|
|
354
634
|
),
|
|
355
635
|
|
|
636
|
+
// Field 3: maxStepTokens
|
|
637
|
+
React.createElement(
|
|
638
|
+
'div',
|
|
639
|
+
{ className: 'ug-field-card' },
|
|
640
|
+
React.createElement(
|
|
641
|
+
'div',
|
|
642
|
+
{ className: 'ug-row' },
|
|
643
|
+
React.createElement('label', { className: 'ug-row-title', htmlFor: 'ug-tokens-input' }, t('maxStepTokens')),
|
|
644
|
+
React.createElement('input', {
|
|
645
|
+
id: 'ug-tokens-input',
|
|
646
|
+
type: 'number',
|
|
647
|
+
className: 'ug-number-input',
|
|
648
|
+
min: 0,
|
|
649
|
+
step: 10000,
|
|
650
|
+
value: draft.maxStepTokens ?? 1000000,
|
|
651
|
+
onChange: (e) => handleNumberChange('maxStepTokens', e.target.value),
|
|
652
|
+
})
|
|
653
|
+
),
|
|
654
|
+
React.createElement('div', { className: 'ug-field-desc' }, t('maxStepTokensDesc'))
|
|
655
|
+
),
|
|
656
|
+
|
|
356
657
|
// Feedback message
|
|
357
658
|
msg.text
|
|
358
659
|
? React.createElement(
|
|
@@ -362,7 +663,7 @@ window.__ModuleLoader__.load({
|
|
|
362
663
|
)
|
|
363
664
|
: null,
|
|
364
665
|
|
|
365
|
-
//
|
|
666
|
+
// Save action
|
|
366
667
|
React.createElement(
|
|
367
668
|
'div',
|
|
368
669
|
{ style: { display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '4px' } },
|
|
@@ -376,14 +677,23 @@ window.__ModuleLoader__.load({
|
|
|
376
677
|
},
|
|
377
678
|
saving ? t('saving') : t('save')
|
|
378
679
|
)
|
|
379
|
-
)
|
|
680
|
+
),
|
|
681
|
+
|
|
682
|
+
// Live Telemetry section
|
|
683
|
+
React.createElement(TelemetrySection, { t }),
|
|
684
|
+
|
|
685
|
+
// Updater section
|
|
686
|
+
React.createElement(UpdaterSection, { t })
|
|
380
687
|
);
|
|
381
688
|
}
|
|
382
689
|
|
|
383
690
|
// Accordion PluginCard for settings.plugin.item
|
|
384
691
|
function PluginCard(props) {
|
|
385
692
|
const [open, setOpen] = React.useState(false);
|
|
386
|
-
const
|
|
693
|
+
const hostCtx = (props && props.ctx) || ctx;
|
|
694
|
+
const locale = getActiveLocale(hostCtx);
|
|
695
|
+
const dict = locale === 'zh' ? zh : en;
|
|
696
|
+
const t = props?.t || makeT(dict, en);
|
|
387
697
|
|
|
388
698
|
React.useEffect(() => {
|
|
389
699
|
ensureCss();
|
|
@@ -430,7 +740,7 @@ window.__ModuleLoader__.load({
|
|
|
430
740
|
null,
|
|
431
741
|
React.createElement(SettingsPage, {
|
|
432
742
|
...props,
|
|
433
|
-
ctx:
|
|
743
|
+
ctx: hostCtx,
|
|
434
744
|
t,
|
|
435
745
|
})
|
|
436
746
|
)
|
|
@@ -438,10 +748,10 @@ window.__ModuleLoader__.load({
|
|
|
438
748
|
);
|
|
439
749
|
}
|
|
440
750
|
|
|
441
|
-
function refreshMirrorUntilVisible(
|
|
751
|
+
function refreshMirrorUntilVisible(c) {
|
|
442
752
|
const visible = () => {
|
|
443
753
|
try {
|
|
444
|
-
const s = (
|
|
754
|
+
const s = (c?.get && c.get('lanSettings')) || c?.settingsScope;
|
|
445
755
|
const view = s?.describe?.()?.getSnapshot?.()?.view;
|
|
446
756
|
return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS);
|
|
447
757
|
} catch (_) {
|
|
@@ -457,7 +767,7 @@ window.__ModuleLoader__.load({
|
|
|
457
767
|
}
|
|
458
768
|
tries += 1;
|
|
459
769
|
try {
|
|
460
|
-
const s = (
|
|
770
|
+
const s = (c?.get && c.get('lanSettings')) || c?.settingsScope;
|
|
461
771
|
s?.describe?.()?.load?.();
|
|
462
772
|
} catch (_) {}
|
|
463
773
|
}, 1000);
|
|
@@ -482,14 +792,14 @@ window.__ModuleLoader__.load({
|
|
|
482
792
|
if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
483
793
|
if (typeof ctx.effect === 'function') {
|
|
484
794
|
ctx.effect(() => {
|
|
485
|
-
const undo = [addLocale('en', en), addLocale('
|
|
795
|
+
const undo = [addLocale('en', en), addLocale('zh', zh)];
|
|
486
796
|
return () => {
|
|
487
797
|
for (const off of undo) off();
|
|
488
798
|
};
|
|
489
799
|
}, 'dsh-usage-guard: dictionaries');
|
|
490
800
|
} else {
|
|
491
801
|
addLocale('en', en);
|
|
492
|
-
addLocale('
|
|
802
|
+
addLocale('zh', zh);
|
|
493
803
|
}
|
|
494
804
|
}
|
|
495
805
|
|
|
@@ -549,6 +859,7 @@ window.__ModuleLoader__.load({
|
|
|
549
859
|
makeT,
|
|
550
860
|
ensureCss,
|
|
551
861
|
createErrorBoundary,
|
|
862
|
+
getActiveLocale,
|
|
552
863
|
};
|
|
553
864
|
return module.exports;
|
|
554
865
|
},
|