@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/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 ru = {
34
- title: 'Usage Guard',
35
- subtitle: 'Санитайзер расхода токенов и защита от сбоев сессий',
36
- header_desc: 'Защищает проекции истории сессий от повреждённых или отсутствующих счётчиков токенов всех LLM-провайдеров.',
37
- badge_active: 'Защита активна',
38
- badge_report_only: 'Только аудит',
39
- 'settings.loading': 'Загрузка настроек Usage Guard…',
40
- 'settings.unavailable': 'Настройки недоступны (пространство хоста ещё не готово).',
41
- 'settings.retry': 'Повторить попытку',
42
- saved: 'Настройки успешно сохранены',
43
- save: 'Сохранить изменения',
44
- saving: 'Сохранение…',
45
- repair: 'Автоматическая починка счётчиков',
46
- repairDesc: 'Подставлять 0 вместо пропущенных или нечисловых счётчиков до сложения ядра, предотвращая зависание истории с ошибкой NaN.',
47
- report: 'Диагностические предупреждения',
48
- reportDesc: 'Выводить в системный журнал подробную строку с указанием сессии, хода, шага и сырой порции при обнаружении повреждений.',
49
- stats_title: 'Статус защиты',
50
- stats_mode: 'Текущий режим',
51
- stats_mode_repair: 'Активная починка (Безопасно)',
52
- stats_mode_report: 'Только аудит (Небезопасно)',
53
- stats_scope: 'Целевая служба',
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, #6366f1);cursor:pointer}
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(200px, 1fr));gap:10px}
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', fontSize: '12px', padding: '4px 10px' },
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
- // Actions
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 t = props?.t || makeT(ru, en);
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: (props && props.ctx) || 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(ctx) {
751
+ function refreshMirrorUntilVisible(c) {
442
752
  const visible = () => {
443
753
  try {
444
- const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope;
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 = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope;
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('ru', ru)];
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('ru', ru);
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
  },