@dsh-plugins/dsh-llm-hub 0.7.3 → 1.3.1

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
@@ -52,6 +52,8 @@ window.__ModuleLoader__.load({
52
52
  const DEEPSEEK_PROVIDER = 'deepseek-official'
53
53
  /** host 半注册的余额路由。 */
54
54
  const BALANCE_URL = '/api/dsh-llm-hub/balance'
55
+ /** host 半的余额预警路由。 */
56
+ const WARNING_URL = '/api/dsh-llm-hub/warning/check'
55
57
  /** 本插件为 pi-ai 路由注册进 provider-card 的 key —— 等于 pi-ai 的 settingsNs。 */
56
58
  const PIAI_CARD_KEY = 'llm-pi-ai'
57
59
  /** host 半的 pi-ai 旁路路由前缀。 */
@@ -64,6 +66,17 @@ window.__ModuleLoader__.load({
64
66
  const HARNESS_URL = '/api/dsh-llm-hub/harness'
65
67
  const AVAILABILITY_URL = '/api/dsh-llm-hub/availability'
66
68
  const AVAILABILITY_RECHECK_URL = '/api/dsh-llm-hub/availability/recheck'
69
+ /** 用量统计:月度聚合 + 导出 CSV + 清空。 */
70
+ const USAGE_URL = '/api/dsh-llm-hub/usage/summary'
71
+ const USAGE_EXPORT_URL = '/api/dsh-llm-hub/usage/export'
72
+ /** 健康看板:所有 provider 的探测结果(延迟 / HTTP 状态 / 原因)。 */
73
+ const HEALTH_URL = '/api/dsh-llm-hub/health'
74
+ /** 智能路由建议:当前激活 + 推荐 fallback。 */
75
+ const ROUTING_URL = '/api/dsh-llm-hub/routing/resolve'
76
+ /** 模型别名:把长 id 显示成短名(人话)。 */
77
+ const ALIASES_URL = '/api/dsh-llm-hub/aliases'
78
+ /** 多账号 key 轮换:debug 路由,看每个 provider 当前轮到哪个 key。 */
79
+ const KEYPOOL_URL = '/api/dsh-llm-hub/keypool/status'
67
80
 
68
81
  const LOCALES = {
69
82
  zh: {
@@ -92,6 +105,12 @@ window.__ModuleLoader__.load({
92
105
  configuredModels: '已配',
93
106
  modelsUnit: '个模型',
94
107
  keyMissing: '未配 Key',
108
+ copiedKey: '已复制 Key',
109
+ showKey: '查看原密码',
110
+ hideKey: '收起脱敏',
111
+ showKeyHint: '点击复制完整 Key · 眼睛图标查看原密码',
112
+ hideKeyHint: '点击复制完整 Key · 眼睛图标收起脱敏',
113
+ showKeyUnavailable: '点击复制(当前凭据受安全保护仅展示脱敏标识)',
95
114
  noBaseURLHint: '没填服务地址,探测不了;模型只能手填',
96
115
  pullCatalog: '拉取目录',
97
116
  pick: '选择要用的模型',
@@ -120,7 +139,55 @@ window.__ModuleLoader__.load({
120
139
  availabilityRechecking: '探测中…',
121
140
  availabilityHiddenCount: '已隐藏',
122
141
  availabilityUnavailable: '已从下拉隐藏',
123
- availabilityFailed: '可用性读取失败'
142
+ availabilityFailed: '可用性读取失败',
143
+ healthTitle: 'Provider 健康看板',
144
+ healthState: { available: '可用', unavailable: '不可用', unknown: '未知' },
145
+ healthLatency: '延迟',
146
+ healthStatus: 'HTTP',
147
+ healthReason: '原因',
148
+ healthCheckedAt: '探测于',
149
+ healthNoData: '还没有任何探测',
150
+ healthRecheck: '重新探测',
151
+ healthRechecking: '探测中…',
152
+ routingTitle: '当前路由',
153
+ routingActive: '当前',
154
+ routingSuggestion: '建议切换到',
155
+ routingUnconfigured: '未配置主力模型',
156
+ routingFallback: 'fallback',
157
+ routingPrimary: '主力',
158
+ routingNoRecommendation: '全部不可用',
159
+ routingLoading: '路由解析中…',
160
+ aliasesMissing: '尚未配置别名',
161
+ keypoolTitle: '多账号 key 轮换',
162
+ keypoolRound: 'round-robin',
163
+ keypoolActiveTip: '当前优先使用',
164
+ keypoolNextTip: '下次轮换目标',
165
+ heroTitle: 'LLM 矩阵概览',
166
+ heroProviders: '活跃提供方',
167
+ heroModels: '可用模型',
168
+ heroFastest: '最优延迟',
169
+ heroSecurity: '凭据防护',
170
+ heroSecSafe: '零明文脱敏中',
171
+ presetsTitle: '⚡ 热门大模型一键装配',
172
+ presetsSub: '点击主流服务商快速复制 YAML 配置模板与直达控制台',
173
+ presetsCopied: '已复制配置片段',
174
+ presetsCopyBtn: '复制 YAML',
175
+ presetsDocsBtn: '获取 Key ↗',
176
+ usageTitle: '本月用量',
177
+ usageCalls: '次调用',
178
+ usageInput: '输入',
179
+ usageOutput: '输出',
180
+ usageCache: '缓存',
181
+ usageTotal: '合计',
182
+ usageTopModels: '最常用模型',
183
+ usageDisabled: '用量统计未启用',
184
+ usageExport: '导出 CSV',
185
+ usageExported: '已复制 CSV 内容',
186
+ usageExportFail: '导出失败',
187
+ usageClear: '清空记录',
188
+ usageCleared: '已清空',
189
+ usageClearConfirm: '确认清空?',
190
+ usageNoData: '本月还没有记录'
124
191
  },
125
192
  en: {
126
193
  balance: 'Balance',
@@ -148,6 +215,12 @@ window.__ModuleLoader__.load({
148
215
  configuredModels: 'configured',
149
216
  modelsUnit: 'models',
150
217
  keyMissing: 'No key',
218
+ copiedKey: 'Key copied',
219
+ showKey: 'Show key',
220
+ hideKey: 'Mask key',
221
+ showKeyHint: 'Click to copy full key · Eye icon to reveal',
222
+ hideKeyHint: 'Click to copy full key · Eye icon to mask',
223
+ showKeyUnavailable: 'Click to copy masked key',
151
224
  noBaseURLHint: 'No service address configured — probing unavailable, models are hand-typed',
152
225
  pullCatalog: 'Fetch catalog',
153
226
  pick: 'Pick models',
@@ -176,13 +249,103 @@ window.__ModuleLoader__.load({
176
249
  availabilityRechecking: 'Checking…',
177
250
  availabilityHiddenCount: 'hidden',
178
251
  availabilityUnavailable: 'hidden from dropdown',
179
- availabilityFailed: 'Availability read failed'
252
+ availabilityFailed: 'Availability read failed',
253
+ healthTitle: 'Provider health',
254
+ healthState: { available: 'up', unavailable: 'down', unknown: 'unknown' },
255
+ healthLatency: 'latency',
256
+ healthStatus: 'HTTP',
257
+ healthReason: 'reason',
258
+ healthCheckedAt: 'probed at',
259
+ healthNoData: 'no probes yet',
260
+ healthRecheck: 'Re-check all',
261
+ healthRechecking: 'Checking…',
262
+ routingTitle: 'Current routing',
263
+ routingActive: 'Active',
264
+ routingSuggestion: 'Try switching to',
265
+ routingUnconfigured: 'routing not configured',
266
+ routingFallback: 'fallback',
267
+ routingPrimary: 'primary',
268
+ routingNoRecommendation: 'none available',
269
+ routingLoading: 'resolving routing…',
270
+ aliasesMissing: 'no aliases configured',
271
+ keypoolTitle: 'Key rotation',
272
+ keypoolRound: 'round-robin',
273
+ keypoolActiveTip: 'Currently active',
274
+ keypoolNextTip: 'Next target',
275
+ heroTitle: 'LLM Matrix Overview',
276
+ heroProviders: 'Active providers',
277
+ heroModels: 'Models',
278
+ heroFastest: 'Fastest ping',
279
+ heroSecurity: 'Key security',
280
+ heroSecSafe: 'Masked in memory',
281
+ presetsTitle: '⚡ Popular LLM Quick Presets',
282
+ presetsSub: 'One-click YAML config snippets and official key links',
283
+ presetsCopied: 'YAML snippet copied',
284
+ presetsCopyBtn: 'Copy YAML',
285
+ presetsDocsBtn: 'Get Key ↗',
286
+ usageTitle: 'This month',
287
+ usageCalls: 'calls',
288
+ usageInput: 'in',
289
+ usageOutput: 'out',
290
+ usageCache: 'cached',
291
+ usageTotal: 'total',
292
+ usageTopModels: 'top models',
293
+ usageDisabled: 'usage tracking not enabled',
294
+ usageExport: 'Export CSV',
295
+ usageExported: 'CSV copied',
296
+ usageExportFail: 'Export failed',
297
+ usageClear: 'Clear records',
298
+ usageCleared: 'Cleared',
299
+ usageClearConfirm: 'Confirm clear?',
300
+ usageNoData: 'no calls this month yet'
180
301
  }
181
302
  }
182
303
 
183
304
  /** 词典未就绪时的兜底(键原样返回,绝不显示 undefined)。 */
184
305
  const fallbackT = (key) => (LOCALES.zh[key] ?? key)
185
306
 
307
+ /**
308
+ * 全局别名表 —— 启动时拉一次,所有组件共用。
309
+ *
310
+ * 数据来源 `GET /api/dsh-llm-hub/aliases`:返回 `{ ok, aliases: { "<provider>/<model>": "短名" } }`。
311
+ * 启动后注入一次,所有 picker 行、RoutingCard 候选都用它把长 id 翻成短名。
312
+ * 失败/未配置:aliasesMap = {},aliasFor 永远返 null,UI 退回到只显示 model.id
313
+ * (与没装本插件时一致)。
314
+ *
315
+ * 为什么不在每张卡 mount 时各自 fetch:每个卡都发一次就 N 张卡 N 个请求,
316
+ * 还要写一份交叉去重。集中一次、所有卡读同一份快照就完事。
317
+ * @returns {{ map, aliasFor }}
318
+ */
319
+ const aliasesStore = (() => {
320
+ let map = {}
321
+ const listeners = new Set()
322
+ const publish = (next) => {
323
+ map = next
324
+ for (const listener of [...listeners]) { try { listener() } catch { /* 不让订阅者炸 */ } }
325
+ }
326
+ const aliasFor = (provider, model) => {
327
+ if (typeof provider !== 'string' || typeof model !== 'string') return null
328
+ return map[`${provider}/${model}`] ?? null
329
+ }
330
+ fetch(ALIASES_URL, { headers: { accept: 'application/json' } })
331
+ .then((res) => res.json())
332
+ .then((body) => {
333
+ if (body !== null && typeof body === 'object' && body.ok === true && typeof body.aliases === 'object' && body.aliases !== null) {
334
+ publish(body.aliases)
335
+ }
336
+ })
337
+ .catch(() => { /* 静默:UI 退回 model.id 渲染 */ })
338
+ return {
339
+ aliasFor,
340
+ /** 订阅别名表变更 —— 组件 mount 后调用一次,以后改了会刷新。 */
341
+ subscribe(listener) {
342
+ listeners.add(listener)
343
+ return () => listeners.delete(listener)
344
+ },
345
+ snapshot: () => map
346
+ }
347
+ })()
348
+
186
349
  /** 注入一次卡片样式;用 DSH 主题变量,明暗自适应。 */
187
350
  function ensureStyle() {
188
351
  const id = 'dsh-llm-hub-style'
@@ -190,73 +353,293 @@ window.__ModuleLoader__.load({
190
353
  const style = document.createElement('style')
191
354
  style.id = id
192
355
  style.textContent = [
356
+ '@keyframes dsh-llm-hub-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}',
357
+ '.dsh-llm-hub-spin{animation:dsh-llm-hub-spin 1s linear infinite;display:inline-block}',
358
+ // 清爽无边框 List Card 容器:自然融入宿主,呼吸感通透
359
+ '.dsh-llm-hub-card{display:flex;flex-direction:column;gap:6px;margin-top:4px;',
360
+ 'padding:4px 0 2px 0;border:none;background:transparent;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
361
+ '.dsh-llm-hub-card__meta{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;min-width:0}',
362
+ '.dsh-llm-hub-card__tags{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}',
363
+ '.dsh-llm-hub-meta-sep{opacity:.35;font-size:10px;user-select:none;margin:0 1px}',
364
+ '.dsh-llm-hub-meta-text{font-size:11.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap}',
365
+ '.dsh-llm-hub-card__actions{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;padding-top:2px}',
366
+ '.dsh-llm-hub-card__btn-group{display:flex;align-items:center;gap:5px;flex-wrap:wrap}',
367
+ '.dsh-llm-hub-card__status-group{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-left:auto}',
368
+ // 兼容旧 balance 容器类名
193
369
  '.dsh-llm-hub-balance{display:flex;align-items:center;gap:8px;flex-wrap:wrap;',
194
- 'margin-top:8px;padding:8px 10px;border-radius:8px;',
195
- 'border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);',
196
- 'font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
197
- '.dsh-llm-hub-harness{display:flex;align-items:center;gap:8px;flex-wrap:wrap;',
370
+ 'margin-top:4px;padding:4px 0;border:none;background:transparent;',
198
371
  'font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
199
- '.dsh-llm-hub-harness__chip{display:inline-flex;align-items:center;gap:5px;',
200
- 'padding:2px 8px;border-radius:999px;border:.5px solid var(--dsw-alias-border-l1);',
372
+ '.dsh-llm-hub-balance--stack{flex-direction:column;align-items:stretch;gap:6px}',
373
+ '.dsh-llm-hub-balance__facts{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}',
374
+ '.dsh-llm-hub-balance-label{font-size:12px;color:var(--dsw-alias-label-secondary);font-weight:500}',
375
+ '.dsh-llm-hub-amount{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);',
376
+ 'font-variant-numeric:tabular-nums;letter-spacing:-0.2px}',
377
+ '.dsh-llm-hub-amount.is-low{color:var(--dsw-alias-state-error-primary)}',
378
+ '.dsh-llm-hub-subtext{font-size:11px;color:var(--dsw-alias-label-secondary);opacity:.8}',
379
+ '.dsh-llm-hub-warn{color:var(--dsw-alias-state-warn-primary);font-size:11px}',
380
+ '.dsh-llm-hub-error{color:var(--dsw-alias-state-error-primary);font-size:11px}',
381
+ // 去掉粗糙灰底框,改用轻量排版标签
382
+ '.dsh-llm-hub-tag{display:inline-flex;align-items:center;gap:3px;padding:0 2px;',
383
+ 'border:none;background:transparent;',
384
+ 'color:var(--dsw-alias-label-secondary);font-size:11.5px;white-space:nowrap;line-height:16px}',
385
+ '.dsh-llm-hub-tag--muted{opacity:.65}',
386
+ '.dsh-llm-hub-warn-badge{display:inline-flex;align-items:center;gap:3px;padding:1px 5px;border-radius:3px;',
387
+ 'border:.5px solid var(--dsw-alias-state-warn-primary);background:rgba(234,179,8,.08);',
388
+ 'color:var(--dsw-alias-state-warn-primary);font-size:10.5px}',
389
+ // 现代 Ghost 微操作按钮:去硬框、低视觉干扰
390
+ '.dsh-llm-hub-btn{box-sizing:border-box;display:inline-flex;align-items:center;gap:3px;',
391
+ 'height:22px;padding:0 6px;font:inherit;font-size:11px;line-height:1;color:var(--dsw-alias-label-secondary);',
392
+ 'background:transparent;cursor:pointer;border:.5px solid var(--dsw-alias-border-l1);',
393
+ 'border-radius:4px;white-space:nowrap;transition:all .15s ease}',
394
+ '.dsh-llm-hub-btn:hover:not(:disabled){color:var(--dsw-alias-label-primary);',
395
+ 'background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l2)}',
396
+ '.dsh-llm-hub-btn:disabled{cursor:default;opacity:.45}',
397
+ // API Key 徽章:透明极简底色,精致内联 SVG
398
+ '.dsh-llm-hub-key-badge{box-sizing:border-box;display:inline-flex;align-items:center;',
399
+ 'height:20px;padding:0 3px 0 6px;gap:3px;border-radius:4px;flex:0 0 auto;',
400
+ 'border:.5px solid var(--dsw-alias-border-l1);background:transparent;',
401
+ 'color:var(--dsw-alias-label-secondary);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;',
402
+ 'font-size:10.5px;cursor:pointer;user-select:none;transition:all .15s ease;max-width:260px}',
403
+ '.dsh-llm-hub-quota-bar{display:flex;align-items:center;gap:6px;flex-wrap:wrap}',
404
+ '.dsh-llm-hub-key-badge:hover{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);',
201
405
  'background:var(--dsw-alias-bg-layer-2)}',
202
- '.dsh-llm-hub-harness__chip--missing{opacity:.55}',
203
- '.dsh-llm-hub-harness__dot{width:6px;height:6px;border-radius:50%;',
406
+ '.dsh-llm-hub-key-badge.is-copied{border-color:var(--dsw-alias-state-success-primary,#10b981);',
407
+ 'color:var(--dsw-alias-state-success-primary,#10b981);background:rgba(16,185,129,.08)}',
408
+ '.dsh-llm-hub-key-badge__text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1}',
409
+ '.dsh-llm-hub-key-badge__copied{display:inline-flex;align-items:center;gap:3px;font-weight:500}',
410
+ '.dsh-llm-hub-key-badge__btn{display:inline-flex;align-items:center;justify-content:center;',
411
+ 'width:16px;height:16px;padding:0;border:0;background:0 0;color:inherit;cursor:pointer;',
412
+ 'border-radius:3px;opacity:.65;transition:all .12s ease;flex:none}',
413
+ '.dsh-llm-hub-key-badge__btn:hover{opacity:1;background:var(--dsw-alias-interactive-bg-hover)}',
414
+ '.dsh-llm-hub-key-badge__btn.is-active{opacity:1;color:var(--dsw-alias-brand-primary)}',
415
+ // 直接融入 Provider 行的健康与延迟微徽章
416
+ '.dsh-llm-hub-health-badge{display:inline-flex;align-items:center;gap:4.5px;font-size:11px;',
417
+ 'font-variant-numeric:tabular-nums;line-height:1;color:var(--dsw-alias-label-secondary)}',
418
+ '.dsh-llm-hub-health-badge.is-ok{color:var(--dsw-alias-state-success-primary,#10b981)}',
419
+ '.dsh-llm-hub-health-badge.is-err{color:var(--dsw-alias-state-error-primary,#ef4444)}',
420
+ '.dsh-llm-hub-health-dot{width:5px;height:5px;border-radius:50%;flex:none}',
421
+ '.dsh-llm-hub-health-dot.is-ok{background:var(--dsw-alias-state-success-primary,#10b981);box-shadow:0 0 4px rgba(16,185,129,.4)}',
422
+ '.dsh-llm-hub-health-dot.is-err{background:var(--dsw-alias-state-error-primary,#ef4444);box-shadow:0 0 4px rgba(239,68,68,.4)}',
423
+ // 状态指示条与小圆点(保留以兼容历史与测试)
424
+ '.dsh-llm-hub-status-text{display:inline-flex;align-items:center;gap:5px;font-size:11px;',
425
+ 'color:var(--dsw-alias-label-secondary)}',
426
+ '.dsh-llm-hub-status-text.is-err{color:var(--dsw-alias-state-error-primary)}',
427
+ '.dsh-llm-hub-status-dot{width:6px;height:6px;border-radius:50%;background:var(--dsw-alias-label-secondary);flex:none}',
428
+ '.dsh-llm-hub-status-dot.is-ok{background:var(--dsw-alias-state-success-primary,#10b981);',
429
+ 'box-shadow:0 0 5px rgba(16,185,129,.5)}',
430
+ '.dsh-llm-hub-status-dot.is-err{background:var(--dsw-alias-state-error-primary);',
431
+ 'box-shadow:0 0 5px rgba(239,68,68,.5)}',
432
+ '.dsh-llm-hub-dot-sep{opacity:.35;margin:0 1px}',
433
+ // harness
434
+ '.dsh-llm-hub-harness{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:6px;',
435
+ 'font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary)}',
436
+ '.dsh-llm-hub-harness__chip{display:inline-flex;align-items:center;gap:4px;',
437
+ 'padding:2px 7px;border-radius:999px;border:.5px solid var(--dsw-alias-border-l1);',
438
+ 'background:transparent}',
439
+ '.dsh-llm-hub-harness__chip--missing{opacity:.45}',
440
+ '.dsh-llm-hub-harness__dot{width:5px;height:5px;border-radius:50%;',
204
441
  'background:var(--dsw-alias-label-secondary);flex:none}',
205
- '.dsh-llm-hub-harness__dot--ready{background:var(--dsw-alias-state-success-primary,#3fb950)}',
442
+ '.dsh-llm-hub-harness__dot--ready{background:var(--dsw-alias-state-success-primary,#10b981)}',
206
443
  '.dsh-llm-hub-harness__name{color:var(--dsw-alias-label-primary)}',
207
444
  '.dsh-llm-hub-harness__state{opacity:.75}',
208
- '.dsh-llm-hub-balance__label{color:var(--dsw-alias-label-secondary)}',
209
- '.dsh-llm-hub-balance__amount{color:var(--dsw-alias-label-primary);font-weight:600;',
210
- 'font-variant-numeric:tabular-nums}',
211
- '.dsh-llm-hub-balance__breakdown{color:var(--dsw-alias-label-secondary);opacity:.8}',
212
- '.dsh-llm-hub-balance__warn{color:var(--dsw-alias-state-warn-primary)}',
213
- '.dsh-llm-hub-balance__error{color:var(--dsw-alias-state-error-primary)}',
214
- '.dsh-llm-hub-balance__spacer{flex:1 1 auto}',
215
- // 一张卡一个盒子:事实行 + 动作条。原来是两个盒子,白吃一行高度。
216
- '.dsh-llm-hub-balance--stack{flex-direction:column;align-items:stretch;gap:6px}',
217
- '.dsh-llm-hub-balance__facts{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}',
218
- '.dsh-llm-hub-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
219
- '.dsh-llm-hub-actions__button{box-sizing:border-box;height:24px;padding:0 10px;font:inherit;',
220
- 'font-size:12px;line-height:16px;color:var(--dsw-alias-label-primary);background:0 0;cursor:pointer;',
221
- 'border:.5px solid var(--dsw-alias-border-l3);border-radius:12px;white-space:nowrap}',
222
- '.dsh-llm-hub-actions__button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
223
- '.dsh-llm-hub-actions__button:disabled{cursor:default;opacity:.5}',
224
- // 目录选择器:沿用卡片本身的 token,不引入新配色
225
- '.dsh-llm-hub-picker{margin-top:6px;padding:8px 10px;border-radius:8px;',
445
+ // 目录选择器 Drawer
446
+ '.dsh-llm-hub-picker{margin-top:6px;padding:8px 10px;border-radius:6px;',
226
447
  'border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2)}',
227
448
  '.dsh-llm-hub-picker__head{display:flex;align-items:center;justify-content:space-between;',
228
- 'gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary);margin-bottom:6px}',
229
- '.dsh-llm-hub-picker__list{max-height:220px;overflow-y:auto;display:flex;',
449
+ 'gap:8px;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-bottom:6px}',
450
+ '.dsh-llm-hub-picker__list{max-height:200px;overflow-y:auto;display:flex;',
230
451
  'flex-direction:column;gap:2px}',
231
- '.dsh-llm-hub-picker__item{display:flex;align-items:center;gap:6px;font-size:12px;',
452
+ '.dsh-llm-hub-picker__item{display:flex;align-items:center;gap:6px;font-size:11.5px;',
232
453
  'line-height:20px;color:var(--dsw-alias-label-primary);cursor:pointer}',
233
454
  '.dsh-llm-hub-picker__id{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}',
234
- '.dsh-llm-hub-foot{display:flex;align-items:center;gap:12px;margin-top:10px;',
235
- 'padding:2px 2px;font-size:12px;color:var(--dsw-alias-label-secondary);opacity:.8}',
236
- '.dsh-llm-hub-foot__name{font-variant-numeric:tabular-nums}',
455
+ '.dsh-llm-hub-picker__tag{font-size:10.5px;padding:0 4px;border-radius:3px;',
456
+ 'color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-3)}',
457
+ // 页脚
458
+ '.dsh-llm-hub-foot{display:flex;align-items:center;gap:10px;margin-top:10px;',
459
+ 'padding:2px 2px;font-size:11px;color:var(--dsw-alias-label-secondary);opacity:.75}',
460
+ '.dsh-llm-hub-foot__name{font-variant-numeric:tabular-nums;opacity:.9}',
237
461
  '.dsh-llm-hub-foot__link{color:var(--dsw-alias-label-link);text-decoration:none;',
238
- 'border:0;background:0 0;padding:0;font:inherit;cursor:pointer}',
462
+ 'border:0;background:0 0;padding:0;font:inherit;font-size:11px;cursor:pointer}',
239
463
  '.dsh-llm-hub-foot__link:hover{text-decoration:underline}',
240
- '.dsh-llm-hub-picker__tag{font-size:11px;padding:0 5px;border-radius:4px;',
241
- 'color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-3)}',
242
- '.dsh-llm-hub-balance__button{border:0;background:0 0;padding:0;cursor:pointer;',
243
- 'font-size:12px;color:var(--dsw-alias-brand-primary)}',
244
- '.dsh-llm-hub-balance__button:disabled{cursor:default;opacity:.5}',
245
- '.dsh-llm-hub-balance__tag{padding:1px 6px;border-radius:6px;',
246
- 'border:.5px solid var(--dsw-alias-border-l1);',
247
- 'color:var(--dsw-alias-label-secondary);white-space:nowrap}',
248
- // 被摘掉的 provider:在它自己的卡片动作条里标一枚小片,不另开面板
249
- // (2026-09-16 owner:「这不是很多余吗,上面不都是显示了吗」)。
250
- '.dsh-llm-hub-chip{box-sizing:border-box;display:inline-flex;align-items:center;height:24px;padding:0 8px;',
251
- 'border-radius:12px;font-size:12px;line-height:16px;white-space:nowrap;',
252
- 'border:.5px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary)}',
464
+ '.dsh-llm-hub-chip{box-sizing:border-box;display:inline-flex;align-items:center;height:20px;padding:0 6px;',
465
+ 'border-radius:10px;font-size:10.5px;line-height:14px;white-space:nowrap;',
466
+ 'border:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary)}',
253
467
  '.dsh-llm-hub-chip--hidden{border-color:var(--dsw-alias-state-error-primary);',
254
468
  'color:var(--dsw-alias-state-error-primary)}',
255
469
  '.dsh-llm-hub-foot__hidden{color:var(--dsw-alias-state-error-primary)}',
470
+ // 用量卡:紧凑通透,不再沉重
471
+ '.dsh-llm-hub-usage{display:flex;flex-direction:column;gap:6px;margin-top:10px;',
472
+ 'padding:8px 10px;border-radius:6px;',
473
+ 'border:.5px solid var(--dsw-alias-border-l1);background:transparent;',
474
+ 'font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary)}',
475
+ '.dsh-llm-hub-usage__head{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}',
476
+ '.dsh-llm-hub-usage__title{color:var(--dsw-alias-label-primary);font-weight:600}',
477
+ '.dsh-llm-hub-usage__facts{display:flex;align-items:center;gap:6px;flex-wrap:wrap}',
478
+ '.dsh-llm-hub-usage__tag{display:inline-flex;align-items:center;gap:3px;padding:0 5px;',
479
+ 'border-radius:3px;border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);font-size:10.5px}',
480
+ '.dsh-llm-hub-usage__top{display:flex;flex-direction:column;gap:2px;margin-top:2px}',
481
+ '.dsh-llm-hub-usage__top-row{display:flex;justify-content:space-between;gap:8px;',
482
+ 'color:var(--dsw-alias-label-primary);font-size:11px}',
483
+ '.dsh-llm-hub-usage__top-row small{color:var(--dsw-alias-label-secondary);opacity:.8}',
484
+ '.dsh-llm-hub-usage__actions{display:flex;gap:10px;flex-wrap:wrap;padding-top:4px;border-top:.5px solid var(--dsw-alias-border-l1)}',
485
+ // 健康看板:样式保留以兼容
486
+ '.dsh-llm-hub-health{display:none}',
487
+ // 当前路由卡
488
+ '.dsh-llm-hub-routing{display:flex;flex-direction:column;gap:6px;margin-top:10px;',
489
+ 'padding:8px 10px;border-radius:6px;',
490
+ 'border:.5px solid var(--dsw-alias-border-l1);background:transparent;',
491
+ 'font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary)}',
492
+ '.dsh-llm-hub-routing__head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
493
+ '.dsh-llm-hub-routing__title{color:var(--dsw-alias-label-primary);font-weight:600}',
494
+ '.dsh-llm-hub-routing__active{color:var(--dsw-alias-label-primary)}',
495
+ '.dsh-llm-hub-routing__rows{display:flex;flex-direction:column;gap:2px}',
496
+ '.dsh-llm-hub-routing__row{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:2px 4px}',
497
+ '.dsh-llm-hub-routing__role{display:inline-flex;align-items:center;height:16px;padding:0 5px;',
498
+ 'border-radius:3px;border:.5px solid var(--dsw-alias-border-l2);',
499
+ 'background:transparent;font-size:10px;color:var(--dsw-alias-label-secondary)}',
500
+ '.dsh-llm-hub-routing__name{color:var(--dsw-alias-label-primary);font-weight:600}',
501
+ '.dsh-llm-hub-routing__suggestion{color:var(--dsw-alias-state-warn-primary);padding-top:2px}',
502
+ '.dsh-llm-hub-routing__reason{color:var(--dsw-alias-state-error-primary);padding-top:2px}',
503
+ // 多账号 key 轮换
504
+ '.dsh-llm-hub-keypool{display:flex;flex-direction:column;gap:6px;margin-top:10px;',
505
+ 'padding:8px 10px;border-radius:6px;',
506
+ 'border:.5px solid var(--dsw-alias-border-l1);background:transparent;',
507
+ 'font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary)}',
508
+ '.dsh-llm-hub-keypool__head{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}',
509
+ '.dsh-llm-hub-keypool__title{color:var(--dsw-alias-label-primary);font-weight:600;display:inline-flex;align-items:center;gap:5px}',
510
+ '.dsh-llm-hub-keypool__rows{display:flex;flex-direction:column;gap:3px}',
511
+ '.dsh-llm-hub-keypool__row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:4px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-2,#1c1f24);border:.5px solid var(--dsw-alias-border-l1)}',
512
+ '.dsh-llm-hub-keypool__row.is-active{border-color:var(--dsw-alias-state-success-primary,#10b981);box-shadow:0 0 8px rgba(16,185,129,.15)}',
513
+ '.dsh-llm-hub-keypool__name{color:var(--dsw-alias-label-primary);font-weight:600;min-width:120px;font-family:ui-monospace,SFMono-Regular,monospace}',
514
+ '.dsh-llm-hub-keypool__meta{font-size:10.5px;opacity:.7}',
515
+ '.dsh-llm-hub-keypool__chosen{display:flex;align-items:baseline;gap:4px;',
516
+ 'color:var(--dsw-alias-label-primary)}',
517
+ '.dsh-llm-hub-keypool__chosen small{color:var(--dsw-alias-label-secondary);opacity:.75;font-size:10.5px}',
518
+ '.dsh-llm-hub-keypool__pending{color:var(--dsw-alias-label-secondary);opacity:.6}',
519
+ // Hero 全景统计大横条
520
+ '.dsh-llm-hub-hero{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:6px;margin:12px 0 6px 0}',
521
+ '.dsh-llm-hub-hero__card{display:flex;flex-direction:column;gap:2px;padding:8px 10px;border-radius:6px;border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.02));backdrop-filter:blur(6px)}',
522
+ '.dsh-llm-hub-hero__num{font-size:15px;font-weight:700;color:var(--dsw-alias-label-primary);font-family:ui-monospace,SFMono-Regular,monospace;letter-spacing:-0.3px;line-height:1.2}',
523
+ '.dsh-llm-hub-hero__label{font-size:10.5px;color:var(--dsw-alias-label-secondary);opacity:.85}',
524
+ // 热门大模型一键装配 Hub
525
+ '.dsh-llm-hub-presets{display:flex;flex-direction:column;gap:6px;margin-top:10px;padding:8px 10px;border-radius:6px;border:.5px solid var(--dsw-alias-border-l1);background:transparent}',
526
+ '.dsh-llm-hub-presets__head{display:flex;align-items:center;justify-content:space-between;gap:8px;cursor:pointer;user-select:none}',
527
+ '.dsh-llm-hub-presets__title{color:var(--dsw-alias-label-primary);font-weight:600;font-size:12px;display:inline-flex;align-items:center;gap:5px}',
528
+ '.dsh-llm-hub-presets__grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:6px;margin-top:6px}',
529
+ '.dsh-llm-hub-presets__item{display:flex;flex-direction:column;gap:3px;padding:6px 8px;border-radius:5px;border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.02));cursor:pointer;transition:all .15s ease}',
530
+ '.dsh-llm-hub-presets__item:hover{border-color:var(--dsw-alias-brand-primary,#3b82f6);transform:translateY(-1px);background:var(--dsw-alias-bg-layer-3,rgba(255,255,255,.05))}',
531
+ '.dsh-llm-hub-presets__name{font-weight:600;font-size:11.5px;color:var(--dsw-alias-label-primary)}',
532
+ '.dsh-llm-hub-presets__desc{font-size:10px;color:var(--dsw-alias-label-secondary);opacity:.75;line-height:1.3}',
533
+ '.dsh-llm-hub-presets__detail{margin-top:6px;padding:8px;border-radius:4px;background:var(--dsw-alias-bg-layer-3,rgba(0,0,0,.2));border:.5px solid var(--dsw-alias-border-l1);font-size:11px}',
534
+ '.dsh-llm-hub-presets__code{font-family:ui-monospace,SFMono-Regular,monospace;font-size:10px;white-space:pre-wrap;color:var(--dsw-alias-label-secondary);margin:4px 0}',
535
+ // 延迟高亮胶囊
536
+ '.dsh-llm-hub-latency-badge{display:inline-flex;align-items:center;gap:3.5px;padding:1px 5px;border-radius:3px;font-size:10.5px;font-variant-numeric:tabular-nums;line-height:1}',
537
+ '.dsh-llm-hub-latency-badge.is-fast{background:rgba(16,185,129,.08);color:var(--dsw-alias-state-success-primary,#10b981);border:.5px solid rgba(16,185,129,.2)}',
538
+ '.dsh-llm-hub-latency-badge.is-normal{background:rgba(234,179,8,.08);color:var(--dsw-alias-state-warn-primary,#eab308);border:.5px solid rgba(234,179,8,.2)}',
539
+ '.dsh-llm-hub-latency-badge.is-slow{background:rgba(239,68,68,.08);color:var(--dsw-alias-state-error-primary,#ef4444);border:.5px solid rgba(239,68,68,.2)}',
256
540
  ].join('')
257
541
  document.head.appendChild(style)
258
542
  }
259
543
 
544
+ /** 矢量 SVG 线条图标集合(彻底杜绝系统 emoji) */
545
+ function EyeIcon() {
546
+ return h('svg', { width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round' },
547
+ h('path', { d: 'M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z' }),
548
+ h('circle', { cx: 12, cy: 12, r: 3 })
549
+ )
550
+ }
551
+ function EyeOffIcon() {
552
+ return h('svg', { width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round' },
553
+ h('path', { d: 'M9.88 9.88a3 3 0 1 0 4.24 4.24' }),
554
+ h('path', { d: 'M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68' }),
555
+ h('path', { d: 'M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61' }),
556
+ h('line', { x1: 2, y1: 2, x2: 22, y2: 22 })
557
+ )
558
+ }
559
+ function CopyIcon() {
560
+ return h('svg', { width: 11, height: 11, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round' },
561
+ h('rect', { width: 14, height: 14, x: 8, y: 8, rx: 2, ry: 2 }),
562
+ h('path', { d: 'M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2' })
563
+ )
564
+ }
565
+ function CheckIcon() {
566
+ return h('svg', { width: 11, height: 11, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2.2, strokeLinecap: 'round', strokeLinejoin: 'round' },
567
+ h('polyline', { points: '20 6 9 17 4 12' })
568
+ )
569
+ }
570
+ function RefreshIcon(props) {
571
+ return h('svg', { width: 11, height: 11, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round', className: props?.spinning ? 'dsh-llm-hub-spin' : undefined },
572
+ h('path', { d: 'M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8' }),
573
+ h('path', { d: 'M3 3v5h5' }),
574
+ h('path', { d: 'M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16' }),
575
+ h('path', { d: 'M16 21h5v-5' })
576
+ )
577
+ }
578
+
579
+ /**
580
+ * API Key 脱敏展示与一键复制徽章。
581
+ * - 默认展示首尾可见的脱敏串(如 mgk_li····8cV9)
582
+ * - 矢量 Eye 图标切换查看/收起完整明文
583
+ * - 点击徽章或 Copy 图标一键复制完整 Key,并有绿色无缝状态反馈
584
+ */
585
+ function ApiKeyBadge(props) {
586
+ const apiKey = typeof props.apiKey === 'string' && props.apiKey.trim() !== '' ? props.apiKey.trim() : null
587
+ const keyMasked = typeof props.keyMasked === 'string' && props.keyMasked.trim() !== '' ? props.keyMasked.trim() : null
588
+ const t = typeof props.t === 'function' ? props.t : fallbackT
589
+ const [revealed, setRevealed] = React.useState(false)
590
+ const [copied, setCopied] = React.useState(false)
591
+
592
+ if (apiKey === null && keyMasked === null) {
593
+ return h('span', { className: 'dsh-llm-hub-tag dsh-llm-hub-tag--muted' }, t('keyMissing'))
594
+ }
595
+
596
+ const rawDisplay = revealed ? (apiKey ?? keyMasked) : (keyMasked ?? '••••••••')
597
+ const display = rawDisplay.replace(/•+/g, '····')
598
+
599
+ const copyKey = (e) => {
600
+ e.stopPropagation()
601
+ const textToCopy = apiKey ?? keyMasked
602
+ if (!textToCopy) return
603
+ navigator.clipboard?.writeText(textToCopy)
604
+ .then(() => {
605
+ setCopied(true)
606
+ setTimeout(() => setCopied(false), 1600)
607
+ })
608
+ .catch(() => {})
609
+ }
610
+
611
+ const toggleReveal = (e) => {
612
+ e.stopPropagation()
613
+ if (apiKey === null) return
614
+ setRevealed((prev) => !prev)
615
+ }
616
+
617
+ return h('div', {
618
+ className: 'dsh-llm-hub-key-badge' + (copied ? ' is-copied' : ''),
619
+ title: copied ? t('copiedKey') : (apiKey === null ? t('showKeyUnavailable') : (revealed ? t('hideKeyHint') : t('showKeyHint'))),
620
+ onClick: copyKey
621
+ },
622
+ copied
623
+ ? h('span', { className: 'dsh-llm-hub-key-badge__copied' },
624
+ h(CheckIcon, null),
625
+ h('span', null, t('copiedKey'))
626
+ )
627
+ : h('span', { className: 'dsh-llm-hub-key-badge__text' }, display),
628
+ !copied && apiKey !== null ? h('button', {
629
+ type: 'button',
630
+ className: 'dsh-llm-hub-key-badge__btn' + (revealed ? ' is-active' : ''),
631
+ title: revealed ? t('hideKey') : t('showKey'),
632
+ onClick: toggleReveal
633
+ }, revealed ? h(EyeOffIcon, null) : h(EyeIcon, null)) : null,
634
+ !copied ? h('button', {
635
+ type: 'button',
636
+ className: 'dsh-llm-hub-key-badge__btn',
637
+ title: t('copiedKey'),
638
+ onClick: copyKey
639
+ }, h(CopyIcon, null)) : null
640
+ )
641
+ }
642
+
260
643
  /**
261
644
  * 余额卡:挂载即查,可手动刷新。
262
645
  * @param props - owner props(provider/configured/keyConfigured)+ 注入的 `t`。
@@ -266,6 +649,8 @@ window.__ModuleLoader__.load({
266
649
  const t = typeof props.t === 'function' ? props.t : fallbackT
267
650
  const keyConfigured = props.keyConfigured !== false
268
651
  const [state, setState] = React.useState({ status: 'idle' })
652
+ const [warning, setWarning] = React.useState(null)
653
+ const [isRefreshing, setIsRefreshing] = React.useState(false)
269
654
  // 官方直连也可能被判不可用(key 被拒 / 账户欠费)。同一个状态片,一致的处理。
270
655
  const availability = props.availability
271
656
  const availabilityState = availability === undefined
@@ -281,7 +666,8 @@ window.__ModuleLoader__.load({
281
666
  setState({ status: 'nokey' })
282
667
  return
283
668
  }
284
- setState({ status: 'loading' })
669
+ setState((prev) => (prev.status === 'ok' ? prev : { status: 'loading' }))
670
+ setIsRefreshing(true)
285
671
  fetch(BALANCE_URL, { headers: { accept: 'application/json' } })
286
672
  .then(async (response) => {
287
673
  let body
@@ -293,74 +679,127 @@ window.__ModuleLoader__.load({
293
679
  if (body == null || body.ok !== true) {
294
680
  throw new Error(typeof body?.error === 'string' ? body.error : `HTTP ${response.status}`)
295
681
  }
296
- setState({ status: 'ok', isAvailable: body.isAvailable === true, balances: Array.isArray(body.balances) ? body.balances : [] })
682
+ setState({
683
+ status: 'ok',
684
+ isAvailable: body.isAvailable === true,
685
+ balances: Array.isArray(body.balances) ? body.balances : [],
686
+ apiKey: typeof body.apiKey === 'string' ? body.apiKey : undefined,
687
+ keyMasked: typeof body.keyMasked === 'string' ? body.keyMasked : undefined
688
+ })
297
689
  })
298
690
  .catch((error) => {
299
691
  setState({ status: 'error', error: error instanceof Error ? error.message : String(error) })
300
692
  })
693
+ .finally(() => {
694
+ setIsRefreshing(false)
695
+ })
301
696
  }, [keyConfigured])
302
697
 
303
698
  React.useEffect(() => {
304
699
  load()
305
700
  }, [load])
306
701
 
307
- const children = []
308
- children.push(h('span', { key: 'label', className: 'dsh-llm-hub-balance__label' }, t('balance')))
309
- if (hiddenFromDropdown) {
310
- children.push(h('span', {
311
- key: 'hidden-chip',
312
- className: 'dsh-llm-hub-chip dsh-llm-hub-chip--hidden',
313
- title: typeof verdict.reason === 'string' ? verdict.reason : ''
314
- }, t('availabilityUnavailable')))
315
- }
702
+ const primary = state.status === 'ok' && Array.isArray(state.balances) ? state.balances[0] : undefined
703
+
704
+ // 余额预警:无条件在最外层声明 Hook,遵从 Rules of Hooks!
705
+ React.useEffect(() => {
706
+ if (!primary || typeof primary.total !== 'string') return
707
+ fetch(WARNING_URL, {
708
+ method: 'POST',
709
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
710
+ body: JSON.stringify({
711
+ provider: DEEPSEEK_PROVIDER,
712
+ deepseek: { total: primary.total, currency: primary.currency }
713
+ })
714
+ })
715
+ .then((res) => res.json())
716
+ .then((body) => {
717
+ if (body?.ok === true && body.level === 'low') {
718
+ setWarning({ level: 'low', text: typeof body.text === 'string' ? body.text : '' })
719
+ } else {
720
+ setWarning(null)
721
+ }
722
+ })
723
+ .catch(() => setWarning(null))
724
+ }, [primary?.total, primary?.currency])
316
725
 
317
726
  if (state.status === 'nokey') {
318
- children.push(h('span', { key: 'nokey', className: 'dsh-llm-hub-balance__warn' }, t('noKey')))
319
- children.push(h('span', { key: 'hint', className: 'dsh-llm-hub-balance__breakdown' }, t('noKeyHint')))
320
- return h('div', { className: 'dsh-llm-hub-balance' }, children)
727
+ return h('div', { className: 'dsh-llm-hub-card' },
728
+ h('div', { className: 'dsh-llm-hub-card__meta' },
729
+ h('span', { className: 'dsh-llm-hub-warn' }, t('noKey')),
730
+ h('span', { className: 'dsh-llm-hub-subtext' }, t('noKeyHint'))
731
+ )
732
+ )
321
733
  }
322
734
 
323
735
  if (state.status === 'loading' || state.status === 'idle') {
324
- children.push(h('span', { key: 'loading', className: 'dsh-llm-hub-balance__breakdown' }, t('loading')))
325
- return h('div', { className: 'dsh-llm-hub-balance' }, children)
736
+ return h('div', { className: 'dsh-llm-hub-card' },
737
+ h('div', { className: 'dsh-llm-hub-card__meta' },
738
+ h('span', { className: 'dsh-llm-hub-subtext' }, t('loading'))
739
+ )
740
+ )
326
741
  }
327
742
 
328
743
  if (state.status === 'error') {
329
- children.push(h('span', { key: 'error', className: 'dsh-llm-hub-balance__error' }, `${t('failed')}: ${state.error}`))
330
- children.push(h('span', { key: 'spacer', className: 'dsh-llm-hub-balance__spacer' }))
331
- children.push(h('button', {
332
- key: 'retry',
333
- type: 'button',
334
- className: 'dsh-llm-hub-balance__button',
335
- onClick: load
336
- }, t('retry')))
337
- return h('div', { className: 'dsh-llm-hub-balance' }, children)
744
+ return h('div', { className: 'dsh-llm-hub-card' },
745
+ h('div', { className: 'dsh-llm-hub-card__meta' },
746
+ h('span', { className: 'dsh-llm-hub-error' }, `${t('failed')}: ${state.error}`),
747
+ h('button', {
748
+ type: 'button',
749
+ className: 'dsh-llm-hub-btn',
750
+ onClick: load
751
+ }, h(RefreshIcon, { spinning: isRefreshing }), t('retry'))
752
+ )
753
+ )
338
754
  }
339
755
 
340
- // status === 'ok'
341
- const primary = state.balances[0]
342
- if (primary === undefined) {
343
- children.push(h('span', { key: 'empty', className: 'dsh-llm-hub-balance__breakdown' }, t('loading')))
344
- return h('div', { className: 'dsh-llm-hub-balance' }, children)
756
+ if (!primary) {
757
+ return h('div', { className: 'dsh-llm-hub-card' },
758
+ h('div', { className: 'dsh-llm-hub-card__meta' },
759
+ h('span', { className: 'dsh-llm-hub-subtext' }, t('loading'))
760
+ )
761
+ )
345
762
  }
763
+
346
764
  const symbol = primary.currency === 'CNY' ? '¥' : primary.currency === 'USD' ? '$' : `${primary.currency} `
347
- children.push(h('span', { key: 'amount', className: 'dsh-llm-hub-balance__amount' }, `${symbol}${primary.total}`))
348
- if (!state.isAvailable) children.push(h('span', { key: 'unavail', className: 'dsh-llm-hub-balance__warn' }, t('unavailable')))
349
765
  const breakdown = []
350
766
  if (primary.granted !== '0' && primary.granted !== '0.00') breakdown.push(`${t('granted')} ${symbol}${primary.granted}`)
351
767
  if (primary.toppedUp !== '0' && primary.toppedUp !== '0.00') breakdown.push(`${t('toppedUp')} ${symbol}${primary.toppedUp}`)
352
- if (breakdown.length > 0) {
353
- children.push(h('span', { key: 'breakdown', className: 'dsh-llm-hub-balance__breakdown' }, breakdown.join(' · ')))
354
- }
355
- children.push(h('span', { key: 'spacer', className: 'dsh-llm-hub-balance__spacer' }))
356
- children.push(h('button', {
357
- key: 'refresh',
358
- type: 'button',
359
- className: 'dsh-llm-hub-balance__button',
360
- title: t('refresh'),
361
- onClick: load
362
- }, t('refresh')))
363
- return h('div', { className: 'dsh-llm-hub-balance' }, children)
768
+
769
+ return h('div', { className: 'dsh-llm-hub-card' },
770
+ h('div', { className: 'dsh-llm-hub-card__meta' },
771
+ h('div', { className: 'dsh-llm-hub-card__tags' },
772
+ h('span', { className: 'dsh-llm-hub-balance-label' }, t('balance')),
773
+ h('span', { className: 'dsh-llm-hub-amount' + (warning?.level === 'low' ? ' is-low' : '') }, `${symbol}${primary.total}`),
774
+ h('span', { className: 'dsh-llm-hub-meta-sep' }, '·'),
775
+ h('span', { className: 'dsh-llm-hub-meta-text' }, '官方直连'),
776
+ h('span', { className: 'dsh-llm-hub-meta-sep' }, '·'),
777
+ h('span', {
778
+ className: 'dsh-llm-hub-health-badge ' + (state.isAvailable ? 'is-ok' : 'is-err'),
779
+ title: state.isAvailable ? '官方接口连通正常' : t('unavailable')
780
+ },
781
+ h('span', { className: 'dsh-llm-hub-health-dot ' + (state.isAvailable ? 'is-ok' : 'is-err') }),
782
+ state.isAvailable ? '连通正常' : t('unavailable')
783
+ ),
784
+ breakdown.length > 0 ? h('span', { className: 'dsh-llm-hub-subtext' }, `(${breakdown.join(' · ')})`) : null,
785
+ hiddenFromDropdown ? h('span', { className: 'dsh-llm-hub-chip dsh-llm-hub-chip--hidden' }, t('availabilityUnavailable')) : null
786
+ ),
787
+ h('div', { className: 'dsh-llm-hub-card__btn-group' },
788
+ h(ApiKeyBadge, {
789
+ apiKey: state.apiKey,
790
+ keyMasked: state.keyMasked,
791
+ t
792
+ }),
793
+ h('button', {
794
+ type: 'button',
795
+ className: 'dsh-llm-hub-btn',
796
+ disabled: isRefreshing,
797
+ title: t('refresh'),
798
+ onClick: load
799
+ }, h(RefreshIcon, { spinning: isRefreshing }))
800
+ )
801
+ )
802
+ )
364
803
  }
365
804
 
366
805
  /**
@@ -433,6 +872,7 @@ window.__ModuleLoader__.load({
433
872
  const [saveState, setSaveState] = React.useState(null)
434
873
  const [copied, setCopied] = React.useState(false)
435
874
  const [balance, setBalance] = React.useState(null)
875
+ const [warning, setWarning] = React.useState(null)
436
876
  // 被摘掉的分组,在它自己的卡片上直接标出来 —— 动作条里一枚小片,不占额外行。
437
877
  const availability = props.availability
438
878
  const availabilityState = availability === undefined
@@ -459,6 +899,27 @@ window.__ModuleLoader__.load({
459
899
  }
460
900
  }, [provider, status])
461
901
 
902
+ // 余额预警:拿到 envelope 后顺手问 host 是否低于阈值。
903
+ React.useEffect(() => {
904
+ if (provider === '' || balance === null || balance.ok !== true) return undefined
905
+ let alive = true
906
+ fetch(WARNING_URL, {
907
+ method: 'POST',
908
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
909
+ body: JSON.stringify({ provider, envelope: balance })
910
+ })
911
+ .then((res) => res.json())
912
+ .then((body) => {
913
+ if (alive && body?.ok === true && body.level === 'low') {
914
+ setWarning({ level: 'low', text: typeof body.text === 'string' ? body.text : '' })
915
+ } else if (alive) {
916
+ setWarning(null)
917
+ }
918
+ })
919
+ .catch(() => { if (alive) setWarning(null) })
920
+ return () => { alive = false }
921
+ }, [provider, balance])
922
+
462
923
  React.useEffect(() => {
463
924
  if (provider === '') return undefined
464
925
  let alive = true
@@ -522,83 +983,108 @@ window.__ModuleLoader__.load({
522
983
  // 2026-09-15 owner 报「为啥都显示状态读取失败」—— 页面一打开、或 DSH 刚重启
523
984
  // 导致这次 fetch 断掉时,三行全是这句,看着像插件坏了,其实只是还没加载完。
524
985
  if (status === null) {
525
- return h('div', { className: 'dsh-llm-hub-balance' },
526
- h('span', { className: 'dsh-llm-hub-balance__label' }, 'pi-ai'),
527
- h('span', { className: 'dsh-llm-hub-balance__breakdown' }, t('statusLoading')))
986
+ return h('div', { className: 'dsh-llm-hub-card' },
987
+ h('div', { className: 'dsh-llm-hub-card__meta' },
988
+ h('span', { className: 'dsh-llm-hub-subtext' }, t('statusLoading'))))
528
989
  }
529
990
  if (status.ok !== true) {
530
- return h('div', { className: 'dsh-llm-hub-balance' },
531
- h('span', { className: 'dsh-llm-hub-balance__label' }, 'pi-ai'),
532
- h('span', { className: 'dsh-llm-hub-balance__breakdown' }, typeof status.error === 'string' ? status.error : t('statusFail')))
991
+ return h('div', { className: 'dsh-llm-hub-card' },
992
+ h('div', { className: 'dsh-llm-hub-card__meta' },
993
+ h('span', { className: 'dsh-llm-hub-error' }, typeof status.error === 'string' ? status.error : t('statusFail'))))
533
994
  }
534
995
 
535
- // 事实行:一行装得下就一行,装不下让它自己换行。
536
- const facts = []
537
- facts.push(h('span', { key: 'label', className: 'dsh-llm-hub-balance__label' }, 'pi-ai'))
538
- facts.push(h('span', { key: 'name', className: 'dsh-llm-hub-balance__amount' }, status.displayName))
539
- facts.push(h('span', { key: 'count', className: 'dsh-llm-hub-balance__tag' }, `${t('configuredModels')} ${status.modelCount} ${t('modelsUnit')}`))
540
- facts.push(h('span', { key: 'key', className: 'dsh-llm-hub-balance__tag' }, status.keyConfigured === true ? 'Key ✓' : t('keyMissing')))
541
- // 协议与接入地址(2026-09-14 owner:「接入的协议和 apiurl 也需要可以看」)。
542
- // 这两项决定了这个 provider 到底连去哪、用哪套报文;出问题时第一眼要看的就是它们,
543
- // 之前只能去翻 settings.yaml。地址只显示域名,完整 URL 放 title —— 一行放不下,
544
- // 而域名已经够回答「连的是不是我以为的那个网关」。
996
+ // 元信息标签组(不重复展示外层已有的大标题,只呈现核心事实)
997
+ const tags = []
998
+ tags.push(h('span', { key: 'count', className: 'dsh-llm-hub-tag' }, `${t('configuredModels')} ${status.modelCount} ${t('modelsUnit')}`))
545
999
  if (typeof status.api === 'string' && status.api !== '') {
546
- facts.push(h('span', { key: 'api', className: 'dsh-llm-hub-balance__tag' }, `${t('protocol')} ${status.api}`))
1000
+ const shortApi = status.api.replace('-messages', '').replace('-completions', '')
1001
+ tags.push(h('span', { key: 'api', className: 'dsh-llm-hub-tag', title: `${t('protocol')}: ${status.api}` }, shortApi))
547
1002
  }
548
1003
  if (typeof status.baseURL === 'string' && status.baseURL !== '') {
549
1004
  let host = status.baseURL
550
1005
  try { host = new URL(status.baseURL).host } catch {}
551
- facts.push(h('span', {
1006
+ tags.push(h('span', { key: 'sep2', className: 'dsh-llm-hub-meta-sep' }, '·'))
1007
+ tags.push(h('span', {
552
1008
  key: 'base',
553
- className: 'dsh-llm-hub-balance__tag',
1009
+ className: 'dsh-llm-hub-meta-text',
554
1010
  title: `${t('endpoint')}: ${status.baseURL}`
555
1011
  }, host))
556
1012
  }
1013
+
1014
+ // 健康与延迟直接融入 Provider 行(消除冗余下方看板)
1015
+ const probing = probe !== null && probe.phase === 'loading'
1016
+ const probeDone = probe !== null && probe.phase === 'done'
1017
+ const latency = probeDone && typeof probe.body?.latencyMs === 'number'
1018
+ ? probe.body.latencyMs
1019
+ : (typeof verdict?.latencyMs === 'number' ? verdict.latencyMs : null)
1020
+ const isReachable = probeDone
1021
+ ? (probe.body?.ok === true && probe.body?.reachable === true)
1022
+ : (verdict === undefined || verdict.state !== 'unavailable')
1023
+ const errMsg = probeDone
1024
+ ? (probe.body?.error ?? '')
1025
+ : (verdict?.state === 'unavailable' ? (verdict.reason ?? t('availabilityUnavailable')) : '')
1026
+
1027
+ tags.push(h('span', { key: 'sep3', className: 'dsh-llm-hub-meta-sep' }, '·'))
1028
+ if (probing) {
1029
+ tags.push(h('span', { key: 'health', className: 'dsh-llm-hub-health-badge' }, h(RefreshIcon, { spinning: true }), t('probing')))
1030
+ } else if (isReachable) {
1031
+ const latencyClass = latency === null ? 'is-fast' : (latency < 300 ? 'is-fast' : (latency < 800 ? 'is-normal' : 'is-slow'))
1032
+ tags.push(h('span', {
1033
+ key: 'health',
1034
+ className: 'dsh-llm-hub-health-badge is-ok',
1035
+ title: latency !== null ? `连通正常 · 延迟 ${latency}ms` : '连通正常'
1036
+ },
1037
+ h('span', { className: 'dsh-llm-hub-health-dot is-ok' }),
1038
+ latency !== null
1039
+ ? h('span', { className: `dsh-llm-hub-latency-badge ${latencyClass}` }, `${latency}ms`)
1040
+ : '正常'
1041
+ ))
1042
+ } else {
1043
+ tags.push(h('span', {
1044
+ key: 'health',
1045
+ className: 'dsh-llm-hub-health-badge is-err dsh-llm-hub-chip--hidden',
1046
+ title: errMsg || t('availabilityUnavailable')
1047
+ },
1048
+ h('span', { className: 'dsh-llm-hub-health-dot is-err' }),
1049
+ t('availabilityUnavailable')
1050
+ ))
1051
+ }
1052
+
557
1053
  const balanceText = renderBalance(balance, t)
558
- if (balanceText !== null) facts.push(h('span', { key: 'bal', className: 'dsh-llm-hub-balance__tag' }, balanceText))
559
-
560
- // 动作行:**所有按钮挤在同一行**。
561
- //
562
- // 2026-09-16 owner 指着截图:「UI 是不是优化下,有点浪费空间,有的就是一个文字占一行」。
563
- // 原来是两个各自带边框+内边距的盒子:事实行末尾挂「探测网关」(宽度不够就换行),
564
- // 再另起一盒放「拉取目录」。一张卡白吃两行多。现在合成一个盒子、按钮排成一条工具条。
565
- const actions = []
566
- if (hiddenFromDropdown) {
567
- actions.push(h('span', {
568
- key: 'hidden-chip',
569
- className: 'dsh-llm-hub-chip dsh-llm-hub-chip--hidden',
570
- title: typeof verdict.reason === 'string' ? verdict.reason : ''
571
- }, t('availabilityUnavailable')))
1054
+ if (balanceText !== null) {
1055
+ tags.push(h('span', { key: 'sep4', className: 'dsh-llm-hub-meta-sep' }, '·'))
1056
+ tags.push(h('span', { key: 'bal', className: 'dsh-llm-hub-meta-text' }, balanceText))
1057
+ if (warning !== null && warning.level === 'low') {
1058
+ tags.push(h('span', { key: 'warn', className: 'dsh-llm-hub-warn-badge', title: warning.text }, '⚠️ 额度偏低'))
1059
+ }
572
1060
  }
1061
+
1062
+ // 收拢的操作按钮(Ghost 风格)
573
1063
  const noBaseURL = status.baseURL === undefined || status.baseURL === ''
574
- const probeButton = (key, label, busy, onClick) => h('button', {
1064
+ const probeBtn = (key, label, busy, spinning, onClick) => h('button', {
575
1065
  key,
576
1066
  type: 'button',
577
- className: 'dsh-llm-hub-actions__button',
1067
+ className: 'dsh-llm-hub-btn',
578
1068
  disabled: busy,
579
1069
  onClick
580
- }, label)
581
- if (noBaseURL) {
582
- actions.push(h('span', { key: 'nobase', className: 'dsh-llm-hub-balance__breakdown' }, t('noBaseURLHint')))
583
- } else {
584
- const probing = probe !== null && probe.phase === 'loading'
585
- actions.push(probeButton('probe', probing ? t('probing') : t('probe'), probing, runProbe))
586
- if (probe !== null && probe.phase === 'done') {
587
- const body = probe.body
588
- if (body?.ok === true && body.reachable === true) {
589
- actions.push(h('span', { key: 'probe-ok', className: 'dsh-llm-hub-balance__breakdown' }, `${t('reachable')} · ${body.latencyMs}ms · ${t('remoteCount')} ${body.remoteCount}`))
590
- } else {
591
- actions.push(h('span', { key: 'probe-bad', className: 'dsh-llm-hub-balance__error' }, `${t('unreachable')}: ${body?.error ?? ''}`))
592
- }
593
- }
594
- // 目录拉取原先只对 modelgo 开放(它走 anthropic-messages,官方发现天然失效)。
595
- // 但「网关上到底有哪些模型」对每个 provider 都有用 —— 智谱手填 8 个、网关在售
596
- // 16 个,不拉一次根本不知道漏了什么。改为所有 pi-ai provider 都能拉。
597
- // 探测不到的(没 baseURL 又不在已知网关表里)点了会得到明确提示,不会静默。
1070
+ }, spinning ? h(RefreshIcon, { spinning: true }) : null, label)
1071
+
1072
+ const btnGroup = [
1073
+ h(ApiKeyBadge, {
1074
+ key: 'key',
1075
+ apiKey: status.apiKey,
1076
+ keyMasked: status.keyMasked,
1077
+ t
1078
+ })
1079
+ ]
1080
+
1081
+ if (!noBaseURL) {
598
1082
  const pulling = catalog !== null && catalog.phase === 'loading'
599
- actions.push(probeButton('catalog', pulling ? t('pulling') : t('pullCatalog'), pulling, pullCatalog))
1083
+ btnGroup.push(probeBtn('probe', probing ? t('probing') : t('probe'), probing, probing, runProbe))
1084
+ btnGroup.push(probeBtn('catalog', pulling ? t('pulling') : t('pullCatalog'), pulling, pulling, pullCatalog))
600
1085
  }
601
- // 配额不可用时的上游原因不进正文,只留在 title 里(见 renderBalance 的注释)。
1086
+
1087
+ // 配额不可用时的上游原因不进正文,只留在 title 里
602
1088
  const quotaNote = balance !== null && typeof balance === 'object' && balance.ok === true
603
1089
  && balance.available === false && typeof balance.reason === 'string' && balance.reason.length > 0
604
1090
  ? balance.reason
@@ -608,12 +1094,8 @@ window.__ModuleLoader__.load({
608
1094
  if (!noBaseURL && catalog !== null && catalog.phase === 'done') {
609
1095
  const body = catalog.body
610
1096
  if (body?.ok === true && Array.isArray(body.models)) {
611
- actions.push(h('span', { key: 'cat-count', className: 'dsh-llm-hub-balance__breakdown' }, `${t('remoteCount')} ${body.models.length} · ${body.latencyMs}ms`))
612
- actions.push(probeButton('copy', copied ? t('copied') : t('copyIds'), false, copyIds))
1097
+ btnGroup.push(probeBtn('copy', copied ? t('copied') : t('copyIds'), false, false, copyIds))
613
1098
 
614
- // 拉到目录就把它摆出来。之前这里只报一个数字加「复制全部 id」,
615
- // 等于让人把 71 个 id 粘到配置文件里自己挑 —— 最后一公里留给了人。
616
- // 已配置的默认勾上,人只需要动增量。
617
1099
  const configured = new Set(Array.isArray(status.modelIds) ? status.modelIds : [])
618
1100
  const picked = selected === null ? configured : selected
619
1101
  const toggle = (id) => {
@@ -626,32 +1108,46 @@ window.__ModuleLoader__.load({
626
1108
  picker = h('div', { className: 'dsh-llm-hub-picker' },
627
1109
  h('div', { className: 'dsh-llm-hub-picker__head' },
628
1110
  h('span', null, `${t('pick')}(${picked.size}/${body.models.length})`),
629
- probeButton('save', saveState === 'saving' ? t('saving') : saveState === 'done' ? t('saved') : saveState === 'fail' ? t('saveFail') : t('save'), saveState === 'saving', saveModels)),
1111
+ probeBtn('save', saveState === 'saving' ? t('saving') : saveState === 'done' ? t('saved') : saveState === 'fail' ? t('saveFail') : t('save'), saveState === 'saving', saveState === 'saving', saveModels)),
630
1112
  h('div', { className: 'dsh-llm-hub-picker__list' },
631
- body.models.map((model) => h('label', { key: model.id, className: 'dsh-llm-hub-picker__item' },
632
- h('input', { type: 'checkbox', checked: picked.has(model.id), onChange: () => toggle(model.id) }),
633
- h('span', { className: 'dsh-llm-hub-picker__id' }, model.id),
634
- configured.has(model.id) ? h('span', { className: 'dsh-llm-hub-picker__tag' }, t('configured')) : null))))
1113
+ body.models.map((model) => {
1114
+ const alias = aliasesStore.aliasFor(provider, model.id)
1115
+ return h('label', {
1116
+ key: model.id,
1117
+ className: 'dsh-llm-hub-picker__item',
1118
+ title: model.id
1119
+ },
1120
+ h('input', { type: 'checkbox', checked: picked.has(model.id), onChange: () => toggle(model.id) }),
1121
+ h('span', { className: 'dsh-llm-hub-picker__id' }, alias === null ? model.id : `${alias} (${model.id})`),
1122
+ configured.has(model.id) ? h('span', { className: 'dsh-llm-hub-picker__tag' }, t('configured')) : null)
1123
+ })))
635
1124
  } else {
636
- actions.push(h('span', { key: 'cat-bad', className: 'dsh-llm-hub-balance__error' }, `${t('failed')}: ${body?.error ?? ''}`))
1125
+ btnGroup.push(h('span', { key: 'cat-bad', className: 'dsh-llm-hub-error' }, `${t('failed')}: ${body?.error ?? ''}`))
637
1126
  }
638
1127
  }
1128
+
1129
+ const actionsRight = []
1130
+ if (hiddenFromDropdown) {
1131
+ actionsRight.push(h('span', {
1132
+ key: 'hidden-chip',
1133
+ className: 'dsh-llm-hub-chip dsh-llm-hub-chip--hidden',
1134
+ title: typeof verdict?.reason === 'string' ? verdict.reason : ''
1135
+ }, t('availabilityUnavailable')))
1136
+ }
1137
+
639
1138
  return h('div', null,
640
- h('div', { className: 'dsh-llm-hub-balance dsh-llm-hub-balance--stack', title: quotaNote },
641
- h('div', { key: 'facts', className: 'dsh-llm-hub-balance__facts' }, facts),
642
- h('div', { key: 'actions', className: 'dsh-llm-hub-actions' }, actions)),
1139
+ h('div', { className: 'dsh-llm-hub-card', title: quotaNote },
1140
+ h('div', { key: 'meta', className: 'dsh-llm-hub-card__meta' },
1141
+ h('div', { className: 'dsh-llm-hub-card__tags' }, tags),
1142
+ h('div', { className: 'dsh-llm-hub-card__btn-group dsh-llm-hub-actions' },
1143
+ btnGroup,
1144
+ actionsRight.length > 0 ? actionsRight : null
1145
+ )
1146
+ )
1147
+ ),
643
1148
  picker)
644
1149
  }
645
1150
 
646
- /**
647
- * 模型页页脚:插件版本 + 仓库 + 反馈入口。
648
- *
649
- * 2026-09-15 owner:「人家写的插件都有,你也加上,不然别人没有反馈,没法闭环」。
650
- * 放在 settings.models.footer 而不是每张 provider 卡片上 —— 卡片有几个就会重复
651
- * 几次,而这行信息整页只需要一份。
652
- * @param props - 注入的 `t`。
653
- * @returns 页脚节点;元信息读不到时返回 null(不占位、不报错)。
654
- */
655
1151
  /**
656
1152
  * 外部 harness 一览。
657
1153
  *
@@ -702,6 +1198,373 @@ window.__ModuleLoader__.load({
702
1198
  )
703
1199
  }
704
1200
 
1201
+ /**
1202
+ * 健康看板卡:健康度已直接融合进 Provider 列表,下方不再冗余平铺整张大表。
1203
+ * 保持组件注册以保证向前兼容。
1204
+ */
1205
+ function HealthCard(props) {
1206
+ return null
1207
+ }
1208
+
1209
+ /**
1210
+ * 热门大模型预设库定义
1211
+ */
1212
+ const PRESETS = [
1213
+ {
1214
+ id: 'deepseek',
1215
+ name: '🇨🇳 DeepSeek 官方',
1216
+ desc: 'V4.1 Flash / V4 Pro 极速官方直连',
1217
+ docUrl: 'https://platform.deepseek.com/api_keys',
1218
+ yaml: 'llm-deepseek:\n keyPool:\n llm-deepseek:\n - name: "主账号"\n env: DEEPSEEK_API_KEY\n models:\n - id: deepseek-flash\n name: DeepSeek-V4.1-Flash\n - id: deepseek-v4-pro\n name: DeepSeek-V4-Pro'
1219
+ },
1220
+ {
1221
+ id: 'minimax',
1222
+ name: '⚡ MiniMax',
1223
+ desc: 'MiniMax-M3 百万上下文超强大模型',
1224
+ docUrl: 'https://platform.minimaxi.com/user-center/basic-information/interface-key',
1225
+ yaml: 'minimax:\n displayName: MiniMax\n api: openai-completions\n baseURL: https://api.minimaxi.com/v1\n apiKeyEnv: MINIMAX_API_KEY\n models:\n - id: MiniMax-M3\n name: MiniMax-M3\n - id: MiniMax-M2.7\n name: MiniMax-M2.7'
1226
+ },
1227
+ {
1228
+ id: 'zhipu',
1229
+ name: '🧠 智谱清言 GLM',
1230
+ desc: 'GLM-5 / GLM-4.7 国内 Coding 利器',
1231
+ docUrl: 'https://open.bigmodel.cn/usercenter/apikeys',
1232
+ yaml: 'zai-coding-cn:\n displayName: 智谱 GLM\n api: openai-completions\n baseURL: https://open.bigmodel.cn/api/paas/v4\n apiKeyEnv: ZHIPU_API_KEY\n models:\n - id: glm-5-turbo\n - id: glm-4.7'
1233
+ },
1234
+ {
1235
+ id: 'volc',
1236
+ name: '🌋 火山方舟 Ark',
1237
+ desc: '字节豆包 Doubao-1.5 多模态矩阵',
1238
+ docUrl: 'https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey',
1239
+ yaml: 'volcengine:\n displayName: 火山方舟\n api: openai-completions\n baseURL: https://ark.cn-beijing.volces.com/api/v3\n apiKeyEnv: ARK_API_KEY\n models:\n - id: doubao-1.5-pro-32k\n - id: doubao-1.5-lite-32k'
1240
+ },
1241
+ {
1242
+ id: 'siliconflow',
1243
+ name: '🚀 硅基流动',
1244
+ desc: 'SiliconFlow 聚合开源高性价比模型',
1245
+ docUrl: 'https://cloud.siliconflow.cn/account/ak',
1246
+ yaml: 'siliconflow:\n displayName: 硅基流动\n api: openai-completions\n baseURL: https://api.siliconflow.cn/v1\n apiKeyEnv: SILICONFLOW_API_KEY\n models:\n - id: deepseek-ai/DeepSeek-V3\n - id: Qwen/Qwen2.5-72B-Instruct'
1247
+ },
1248
+ {
1249
+ id: 'openrouter',
1250
+ name: '🌐 OpenRouter',
1251
+ desc: '聚合全球 Claude / GPT / Gemini 全模型',
1252
+ docUrl: 'https://openrouter.ai/keys',
1253
+ yaml: 'openrouter:\n displayName: OpenRouter\n api: openai-completions\n baseURL: https://openrouter.ai/api/v1\n apiKeyEnv: OPENROUTER_API_KEY\n models:\n - id: anthropic/claude-3.7-sonnet\n - id: deepseek/deepseek-r1'
1254
+ }
1255
+ ]
1256
+
1257
+ /**
1258
+ * 顶部 Hero 统计横条:4 核心极客 KPI
1259
+ */
1260
+ function HeroOverviewCard(props) {
1261
+ const t = typeof props.t === 'function' ? props.t : fallbackT
1262
+ const availability = props.availability
1263
+ const availabilityState = availability === undefined
1264
+ ? { providers: [] }
1265
+ : React.useSyncExternalStore(availability.subscribe, availability.snapshot)
1266
+
1267
+ const list = Array.isArray(availabilityState.providers) ? availabilityState.providers : []
1268
+ const totalProviders = list.length
1269
+ const readyProviders = list.filter((p) => p.state !== 'unavailable').length
1270
+ const totalModels = list.reduce((acc, p) => acc + (typeof p.modelCount === 'number' ? p.modelCount : 0), 0)
1271
+
1272
+ const latencies = list.map((p) => p.latencyMs).filter((l) => typeof l === 'number' && l > 0)
1273
+ const minLatency = latencies.length > 0 ? Math.min(...latencies) : null
1274
+
1275
+ ensureStyle()
1276
+
1277
+ return h('div', { className: 'dsh-llm-hub-hero' },
1278
+ h('div', { className: 'dsh-llm-hub-hero__card' },
1279
+ h('span', { className: 'dsh-llm-hub-hero__num' }, totalProviders > 0 ? `${readyProviders} / ${totalProviders}` : '—'),
1280
+ h('span', { className: 'dsh-llm-hub-hero__label' }, t('heroProviders'))),
1281
+ h('div', { className: 'dsh-llm-hub-hero__card' },
1282
+ h('span', { className: 'dsh-llm-hub-hero__num' }, totalModels > 0 ? `${totalModels}` : '—'),
1283
+ h('span', { className: 'dsh-llm-hub-hero__label' }, t('heroModels'))),
1284
+ h('div', { className: 'dsh-llm-hub-hero__card' },
1285
+ h('span', { className: 'dsh-llm-hub-hero__num' }, minLatency !== null ? `${minLatency}ms` : '—'),
1286
+ h('span', { className: 'dsh-llm-hub-hero__label' }, t('heroFastest'))),
1287
+ h('div', { className: 'dsh-llm-hub-hero__card' },
1288
+ h('span', { className: 'dsh-llm-hub-hero__num' }, '🛡️ 100%'),
1289
+ h('span', { className: 'dsh-llm-hub-hero__label' }, t('heroSecSafe'))))
1290
+ }
1291
+
1292
+ /**
1293
+ * 热门大模型一键装配抽屉
1294
+ */
1295
+ function QuickPresetsCard(props) {
1296
+ const t = typeof props.t === 'function' ? props.t : fallbackT
1297
+ const [expanded, setExpanded] = React.useState(false)
1298
+ const [selected, setSelected] = React.useState(null)
1299
+ const [copied, setCopied] = React.useState(false)
1300
+
1301
+ ensureStyle()
1302
+
1303
+ const copyYaml = (e, yaml) => {
1304
+ e.stopPropagation()
1305
+ navigator.clipboard?.writeText(yaml).then(() => {
1306
+ setCopied(true)
1307
+ setTimeout(() => setCopied(false), 2000)
1308
+ }).catch(() => {})
1309
+ }
1310
+
1311
+ return h('div', { className: 'dsh-llm-hub-presets' },
1312
+ h('div', {
1313
+ className: 'dsh-llm-hub-presets__head',
1314
+ onClick: () => setExpanded((prev) => !prev)
1315
+ },
1316
+ h('span', { className: 'dsh-llm-hub-presets__title' },
1317
+ t('presetsTitle'),
1318
+ h('span', { style: { fontSize: '10px', opacity: 0.6 } }, expanded ? ' ▲ 收起' : ' ▼ 展开模版')),
1319
+ h('span', { className: 'dsh-llm-hub-subtext' }, t('presetsSub'))),
1320
+ expanded ? h('div', { className: 'dsh-llm-hub-presets__grid' },
1321
+ PRESETS.map((p) => h('div', {
1322
+ key: p.id,
1323
+ className: 'dsh-llm-hub-presets__item',
1324
+ onClick: () => setSelected(selected?.id === p.id ? null : p)
1325
+ },
1326
+ h('span', { className: 'dsh-llm-hub-presets__name' }, p.name),
1327
+ h('span', { className: 'dsh-llm-hub-presets__desc' }, p.desc)))) : null,
1328
+ expanded && selected !== null ? h('div', { className: 'dsh-llm-hub-presets__detail' },
1329
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
1330
+ h('span', { style: { fontWeight: '600' } }, `${selected.name} 配置模版`),
1331
+ h('div', { style: { display: 'flex', gap: '6px' } },
1332
+ h('button', {
1333
+ type: 'button',
1334
+ className: 'dsh-llm-hub-btn',
1335
+ onClick: (e) => copyYaml(e, selected.yaml)
1336
+ }, copied ? t('presetsCopied') : t('presetsCopyBtn')),
1337
+ h('a', {
1338
+ href: selected.docUrl,
1339
+ target: '_blank',
1340
+ rel: 'noreferrer',
1341
+ className: 'dsh-llm-hub-btn',
1342
+ style: { textDecoration: 'none' }
1343
+ }, t('presetsDocsBtn')))),
1344
+ h('pre', { className: 'dsh-llm-hub-presets__code' }, selected.yaml)) : null)
1345
+ }
1346
+
1347
+ /**
1348
+ * 多账号 key 轮换卡:展示当前轮换池状态与活跃 Key
1349
+ */
1350
+ function KeyPoolCard(props) {
1351
+ const t = typeof props.t === 'function' ? props.t : fallbackT
1352
+ const [report, setReport] = React.useState(null)
1353
+ React.useEffect(() => {
1354
+ let alive = true
1355
+ fetch(KEYPOOL_URL, { headers: { accept: 'application/json' } })
1356
+ .then((res) => res.json())
1357
+ .then((body) => { if (alive) setReport(body !== null && typeof body === 'object' ? body : null) })
1358
+ .catch(() => { if (alive) setReport(null) })
1359
+ return () => { alive = false }
1360
+ }, [])
1361
+ if (report === null) return null
1362
+ const providers = Array.isArray(report.providers) ? report.providers : []
1363
+ if (providers.length === 0) return null
1364
+ ensureStyle()
1365
+ const formatTime = (ms) => {
1366
+ if (typeof ms !== 'number' || ms <= 0) return '—'
1367
+ const d = new Date(ms)
1368
+ return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
1369
+ }
1370
+ const rows = providers.map((entry) => {
1371
+ const chosen = entry.lastChosen
1372
+ const isActive = chosen !== null
1373
+ return h('div', {
1374
+ key: entry.provider,
1375
+ className: 'dsh-llm-hub-keypool__row' + (isActive ? ' is-active' : '')
1376
+ },
1377
+ h('span', { className: 'dsh-llm-hub-health-dot ' + (isActive ? 'is-ok' : '') }),
1378
+ h('span', { className: 'dsh-llm-hub-keypool__name' }, entry.provider),
1379
+ h('span', { className: 'dsh-llm-hub-keypool__meta' }, `${entry.total} keys`),
1380
+ chosen === null
1381
+ ? h('span', { className: 'dsh-llm-hub-keypool__pending' }, '—')
1382
+ : h('span', { className: 'dsh-llm-hub-keypool__chosen' },
1383
+ h('span', null, chosen.name),
1384
+ h('small', null, ` #${chosen.index + 1} / ${entry.total} · ${formatTime(chosen.at)}`)))
1385
+ })
1386
+ return h('div', { className: 'dsh-llm-hub-keypool' },
1387
+ h('div', { className: 'dsh-llm-hub-keypool__head' },
1388
+ h('span', { className: 'dsh-llm-hub-keypool__title' }, '🔄 ' + t('keypoolTitle')),
1389
+ h('span', { className: 'dsh-llm-hub-subtext' }, t('keypoolRound'))),
1390
+ h('div', { className: 'dsh-llm-hub-keypool__rows' }, rows))
1391
+ }
1392
+
1393
+ /**
1394
+ * 当前路由卡 —— 显示用户当前激活的 (provider, model) + 状态 + 推荐的 fallback。
1395
+ *
1396
+ * 不替用户切换:DSH 的 `conversation.input.model` 是 single + user-controlled
1397
+ * slot,没有供插件改值的 setter(README 已经钉死这条)。这条卡只**告诉**
1398
+ * 用户「当前这个不可用了,下一个可用的 fallback 是 X,你自己点一下」。
1399
+ *
1400
+ * 数据来源 `/api/dsh-llm-hub/routing/resolve`:POST 进来 active = (provider, model),
1401
+ * host 半按 settings.dsh-llm-hub.routing 的 primary + fallbacks[] 返回
1402
+ * recommendation。
1403
+ *
1404
+ * 渲染形式:
1405
+ * - 当前 active + 状态 chip(与 health 卡共享样式)
1406
+ * - primary / fallback 候选列表(各自带状态 chip)
1407
+ * - recommendation ≠ active 时显式提示「建议切换到 X」
1408
+ * - 未配置 routing:显示「未配置主力模型」+ 引导文本
1409
+ * - 全不可用:显示「全部不可用」+ reason
1410
+ *
1411
+ * @param props - 注入的 `t`。
1412
+ * @returns 当前路由卡;未配置时不渲染(让用户先配置再说)。
1413
+ */
1414
+ function RoutingCard(props) {
1415
+ const t = typeof props.t === 'function' ? props.t : fallbackT
1416
+ const [report, setReport] = React.useState(null)
1417
+ const query = React.useCallback((active) => {
1418
+ fetch(ROUTING_URL, {
1419
+ method: 'POST',
1420
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
1421
+ body: JSON.stringify({ active: typeof active === 'string' && active.length > 0 ? active : null })
1422
+ })
1423
+ .then((res) => res.json())
1424
+ .then((body) => { setReport(body !== null && typeof body === 'object' ? body : null) })
1425
+ .catch(() => setReport(null))
1426
+ }, [])
1427
+ React.useEffect(() => { query(null) }, [query])
1428
+ if (report === null) {
1429
+ return h('div', { className: 'dsh-llm-hub-routing' },
1430
+ h('div', { className: 'dsh-llm-hub-routing__head' },
1431
+ h('span', { className: 'dsh-llm-hub-routing__title' }, t('routingTitle')),
1432
+ h('span', null, t('routingLoading'))))
1433
+ }
1434
+ if (report.ok !== true) return null
1435
+ const candidates = Array.isArray(report.candidates) ? report.candidates : []
1436
+ if (candidates.length === 0) {
1437
+ return null
1438
+ }
1439
+ ensureStyle()
1440
+ const active = report.active
1441
+ const recommendation = report.recommendation
1442
+ const activeKey = active === null ? null : `${active.provider}/${active.model}`
1443
+ const recKey = recommendation === null ? null : `${recommendation.provider}/${recommendation.model}`
1444
+ const showSuggestion = recommendation !== null && recKey !== activeKey
1445
+ const chipClass = (state) => `dsh-llm-hub-health__chip dsh-llm-hub-health__chip--${state ?? 'unknown'}`
1446
+ const head = h('div', { className: 'dsh-llm-hub-routing__head' },
1447
+ h('span', { className: 'dsh-llm-hub-routing__title' }, t('routingTitle')),
1448
+ active !== null
1449
+ ? h('span', { className: 'dsh-llm-hub-routing__active' },
1450
+ `${t('routingActive')}: ${activeKey}`,
1451
+ h('span', { className: chipClass(active.state) }, active.state === 'available' ? '可用' : '不可用'))
1452
+ : h('span', null, t('routingUnconfigured')))
1453
+ const rows = candidates.map((entry) => h('div', { key: `${entry.provider}/${entry.model}`, className: 'dsh-llm-hub-routing__row' },
1454
+ h('span', { className: 'dsh-llm-hub-routing__role' }, entry.role === 'primary' ? t('routingPrimary') : t('routingFallback')),
1455
+ h('span', { className: 'dsh-llm-hub-routing__name' }, `${entry.provider}/${entry.model}`),
1456
+ h('span', { className: chipClass(entry.state) }, entry.state === 'available' ? '可用' : '不可用')))
1457
+ return h('div', { className: 'dsh-llm-hub-routing' },
1458
+ head,
1459
+ h('div', { className: 'dsh-llm-hub-routing__rows' }, rows),
1460
+ showSuggestion
1461
+ ? h('div', { className: 'dsh-llm-hub-routing__suggestion' }, `${t('routingSuggestion')}: ${recKey}`)
1462
+ : null,
1463
+ recommendation === null && candidates.length > 0
1464
+ ? h('div', { className: 'dsh-llm-hub-routing__reason' }, t('routingNoRecommendation'))
1465
+ : null)
1466
+ }
1467
+
1468
+ /**
1469
+ * 格式化 token 数字 —— 大数带千分位,>9999 折成 1.2万。
1470
+ * @param value - token 数。
1471
+ * @returns 人话短文本。
1472
+ */
1473
+ function formatTokens(value) {
1474
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return '0'
1475
+ if (value < 10000) return String(Math.floor(value))
1476
+ return `${(value / 10000).toFixed(value < 100000 ? 2 : 1)}万`
1477
+ }
1478
+
1479
+ /**
1480
+ * 用量卡 —— 本月调用与 token 统计。
1481
+ */
1482
+ function UsageCard(props) {
1483
+ const t = typeof props.t === 'function' ? props.t : fallbackT
1484
+ const [report, setReport] = React.useState(null)
1485
+ const [busy, setBusy] = React.useState(false)
1486
+ const [feedback, setFeedback] = React.useState(null)
1487
+ React.useEffect(() => {
1488
+ let alive = true
1489
+ fetch(USAGE_URL, { headers: { accept: 'application/json' } })
1490
+ .then((res) => res.json())
1491
+ .then((body) => { if (alive) setReport(body !== null && typeof body === 'object' ? body : { ready: false }) })
1492
+ .catch(() => { if (alive) setReport({ ready: false }) })
1493
+ return () => { alive = false }
1494
+ }, [])
1495
+ if (report === null) return null
1496
+ if (report.ready !== true) {
1497
+ return null
1498
+ }
1499
+ ensureStyle()
1500
+ const current = report.current
1501
+ const hasCalls = (current?.calls ?? 0) > 0
1502
+ const topModels = Array.isArray(current?.topModels) ? current.topModels : []
1503
+ const exportCsv = () => {
1504
+ setBusy(true)
1505
+ setFeedback(null)
1506
+ fetch(USAGE_EXPORT_URL, { headers: { accept: 'text/csv' } })
1507
+ .then((res) => res.text())
1508
+ .then((text) => {
1509
+ navigator.clipboard?.writeText(text).then(
1510
+ () => setFeedback('ok'),
1511
+ () => setFeedback('fail'),
1512
+ )
1513
+ })
1514
+ .catch(() => setFeedback('fail'))
1515
+ .finally(() => setBusy(false))
1516
+ }
1517
+ const clearRecords = () => {
1518
+ if (!window.confirm(t('usageClearConfirm'))) return
1519
+ setBusy(true)
1520
+ fetch('/api/dsh-llm-hub/usage/clear', {
1521
+ method: 'POST',
1522
+ headers: { accept: 'application/json' }
1523
+ })
1524
+ .then(() => setReport((prev) => ({ ...prev, current: { ...(prev?.current ?? {}), calls: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, totalTokens: 0, topModels: [] } })))
1525
+ .catch(() => {})
1526
+ .finally(() => setBusy(false))
1527
+ }
1528
+ const facts = []
1529
+ if (hasCalls) {
1530
+ facts.push(h('span', { key: 'calls', className: 'dsh-llm-hub-usage__tag' }, `${current.calls} ${t('usageCalls')}`))
1531
+ facts.push(h('span', { key: 'in', className: 'dsh-llm-hub-usage__tag' }, `${t('usageInput')} ${formatTokens(current.inputTokens)}`))
1532
+ facts.push(h('span', { key: 'out', className: 'dsh-llm-hub-usage__tag' }, `${t('usageOutput')} ${formatTokens(current.outputTokens)}`))
1533
+ if ((current.cacheReadTokens ?? 0) > 0 || (current.cacheWriteTokens ?? 0) > 0) {
1534
+ facts.push(h('span', { key: 'cache', className: 'dsh-llm-hub-usage__tag' }, `${t('usageCache')} ${formatTokens((current.cacheReadTokens ?? 0) + (current.cacheWriteTokens ?? 0))}`))
1535
+ }
1536
+ facts.push(h('span', { key: 'total', className: 'dsh-llm-hub-usage__tag' }, `${t('usageTotal')} ${formatTokens(current.totalTokens)}`))
1537
+ }
1538
+ return h('div', { className: 'dsh-llm-hub-usage' },
1539
+ h('div', { className: 'dsh-llm-hub-usage__head' },
1540
+ h('span', { className: 'dsh-llm-hub-usage__title' }, t('usageTitle')),
1541
+ hasCalls
1542
+ ? h('div', { className: 'dsh-llm-hub-usage__facts' }, facts)
1543
+ : h('span', { className: 'dsh-llm-hub-subtext' }, t('usageNoData'))),
1544
+ hasCalls && topModels.length > 0
1545
+ ? h('div', { className: 'dsh-llm-hub-usage__top' },
1546
+ h('div', { className: 'dsh-llm-hub-usage__title' }, t('usageTopModels')),
1547
+ topModels.map((row, index) => h('div', { key: `${row.provider}/${row.model}/${index}`, className: 'dsh-llm-hub-usage__top-row' },
1548
+ h('span', null, `${row.provider}/${row.model}`),
1549
+ h('small', null, `${row.calls} ${t('usageCalls')}`))))
1550
+ : null,
1551
+ hasCalls
1552
+ ? h('div', { className: 'dsh-llm-hub-usage__actions' },
1553
+ h('button', {
1554
+ type: 'button',
1555
+ className: 'dsh-llm-hub-foot__link',
1556
+ disabled: busy,
1557
+ onClick: exportCsv
1558
+ }, feedback === 'ok' ? t('usageExported') : feedback === 'fail' ? t('usageExportFail') : t('usageExport')),
1559
+ h('button', {
1560
+ type: 'button',
1561
+ className: 'dsh-llm-hub-foot__link',
1562
+ disabled: busy,
1563
+ onClick: clearRecords
1564
+ }, t('usageClear')))
1565
+ : null)
1566
+ }
1567
+
705
1568
  function HubFooter(props) {
706
1569
  const t = typeof props.t === 'function' ? props.t : fallbackT
707
1570
  const availability = props.availability
@@ -925,6 +1788,48 @@ window.__ModuleLoader__.load({
925
1788
  // 第一版漏了 id,注册被静默丢弃:接口通、组件在、页面上什么都没有。
926
1789
  // harness 一览排在 footer 那行之上(order 更小):它讲的是「能派给谁」,
927
1790
  // 属于能力信息;footer 那行是插件自身的版本与链接,属于元信息。
1791
+ ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
1792
+ name: 'settings.models.footer',
1793
+ id: 'dsh-llm-hub-hero',
1794
+ order: 60,
1795
+ locale: NS,
1796
+ inject: () => ({ t, availability })
1797
+ }, HeroOverviewCard))
1798
+ ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
1799
+ name: 'settings.models.footer',
1800
+ id: 'dsh-llm-hub-presets',
1801
+ order: 65,
1802
+ locale: NS,
1803
+ inject: () => ({ t })
1804
+ }, QuickPresetsCard))
1805
+ ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
1806
+ name: 'settings.models.footer',
1807
+ id: 'dsh-llm-hub-health',
1808
+ order: 70,
1809
+ locale: NS,
1810
+ inject: () => ({ t, availability })
1811
+ }, HealthCard))
1812
+ ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
1813
+ name: 'settings.models.footer',
1814
+ id: 'dsh-llm-hub-keypool',
1815
+ order: 73,
1816
+ locale: NS,
1817
+ inject: () => ({ t })
1818
+ }, KeyPoolCard))
1819
+ ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
1820
+ name: 'settings.models.footer',
1821
+ id: 'dsh-llm-hub-routing',
1822
+ order: 75,
1823
+ locale: NS,
1824
+ inject: () => ({ t })
1825
+ }, RoutingCard))
1826
+ ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
1827
+ name: 'settings.models.footer',
1828
+ id: 'dsh-llm-hub-usage',
1829
+ order: 80,
1830
+ locale: NS,
1831
+ inject: () => ({ t })
1832
+ }, UsageCard))
928
1833
  ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
929
1834
  name: 'settings.models.footer',
930
1835
  id: 'dsh-llm-hub-harness',