@dsh-plugins/dsh-llm-hub 1.3.0 → 1.3.2

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
@@ -105,6 +105,12 @@ window.__ModuleLoader__.load({
105
105
  configuredModels: '已配',
106
106
  modelsUnit: '个模型',
107
107
  keyMissing: '未配 Key',
108
+ copiedKey: '已复制 Key',
109
+ showKey: '查看原密码',
110
+ hideKey: '收起脱敏',
111
+ showKeyHint: '点击复制完整 Key · 眼睛图标查看原密码',
112
+ hideKeyHint: '点击复制完整 Key · 眼睛图标收起脱敏',
113
+ showKeyUnavailable: '点击复制(当前凭据受安全保护仅展示脱敏标识)',
108
114
  noBaseURLHint: '没填服务地址,探测不了;模型只能手填',
109
115
  pullCatalog: '拉取目录',
110
116
  pick: '选择要用的模型',
@@ -154,6 +160,19 @@ window.__ModuleLoader__.load({
154
160
  aliasesMissing: '尚未配置别名',
155
161
  keypoolTitle: '多账号 key 轮换',
156
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 ↗',
157
176
  usageTitle: '本月用量',
158
177
  usageCalls: '次调用',
159
178
  usageInput: '输入',
@@ -168,7 +187,12 @@ window.__ModuleLoader__.load({
168
187
  usageClear: '清空记录',
169
188
  usageCleared: '已清空',
170
189
  usageClearConfirm: '确认清空?',
171
- usageNoData: '本月还没有记录'
190
+ usageNoData: '本月还没有记录',
191
+ suiteTitle: 'Webkubor DSH 扩展家族',
192
+ suiteDesc: '一键构建完整智能体开发环境',
193
+ suiteActive: '已激活',
194
+ suiteCopyInstall: '复制安装',
195
+ suiteCopied: '已复制命令'
172
196
  },
173
197
  en: {
174
198
  balance: 'Balance',
@@ -196,6 +220,12 @@ window.__ModuleLoader__.load({
196
220
  configuredModels: 'configured',
197
221
  modelsUnit: 'models',
198
222
  keyMissing: 'No key',
223
+ copiedKey: 'Key copied',
224
+ showKey: 'Show key',
225
+ hideKey: 'Mask key',
226
+ showKeyHint: 'Click to copy full key · Eye icon to reveal',
227
+ hideKeyHint: 'Click to copy full key · Eye icon to mask',
228
+ showKeyUnavailable: 'Click to copy masked key',
199
229
  noBaseURLHint: 'No service address configured — probing unavailable, models are hand-typed',
200
230
  pullCatalog: 'Fetch catalog',
201
231
  pick: 'Pick models',
@@ -245,6 +275,19 @@ window.__ModuleLoader__.load({
245
275
  aliasesMissing: 'no aliases configured',
246
276
  keypoolTitle: 'Key rotation',
247
277
  keypoolRound: 'round-robin',
278
+ keypoolActiveTip: 'Currently active',
279
+ keypoolNextTip: 'Next target',
280
+ heroTitle: 'LLM Matrix Overview',
281
+ heroProviders: 'Active providers',
282
+ heroModels: 'Models',
283
+ heroFastest: 'Fastest ping',
284
+ heroSecurity: 'Key security',
285
+ heroSecSafe: 'Masked in memory',
286
+ presetsTitle: '⚡ Popular LLM Quick Presets',
287
+ presetsSub: 'One-click YAML config snippets and official key links',
288
+ presetsCopied: 'YAML snippet copied',
289
+ presetsCopyBtn: 'Copy YAML',
290
+ presetsDocsBtn: 'Get Key ↗',
248
291
  usageTitle: 'This month',
249
292
  usageCalls: 'calls',
250
293
  usageInput: 'in',
@@ -259,7 +302,12 @@ window.__ModuleLoader__.load({
259
302
  usageClear: 'Clear records',
260
303
  usageCleared: 'Cleared',
261
304
  usageClearConfirm: 'Confirm clear?',
262
- usageNoData: 'no calls this month yet'
305
+ usageNoData: 'no calls this month yet',
306
+ suiteTitle: 'Webkubor DSH Plugin Suite',
307
+ suiteDesc: 'One-click full agent development environment',
308
+ suiteActive: 'Active',
309
+ suiteCopyInstall: 'Copy install',
310
+ suiteCopied: 'Command copied'
263
311
  }
264
312
  }
265
313
 
@@ -315,150 +363,312 @@ window.__ModuleLoader__.load({
315
363
  const style = document.createElement('style')
316
364
  style.id = id
317
365
  style.textContent = [
366
+ '@keyframes dsh-llm-hub-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}',
367
+ '.dsh-llm-hub-spin{animation:dsh-llm-hub-spin 1s linear infinite;display:inline-block}',
368
+ // 清爽无边框 List Card 容器:自然融入宿主,呼吸感通透
369
+ '.dsh-llm-hub-card{display:flex;flex-direction:column;gap:6px;margin-top:4px;',
370
+ 'padding:4px 0 2px 0;border:none;background:transparent;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
371
+ '.dsh-llm-hub-card__meta{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;min-width:0}',
372
+ '.dsh-llm-hub-card__tags{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}',
373
+ '.dsh-llm-hub-meta-sep{opacity:.35;font-size:10px;user-select:none;margin:0 1px}',
374
+ '.dsh-llm-hub-meta-text{font-size:11.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap}',
375
+ '.dsh-llm-hub-card__actions{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;padding-top:2px}',
376
+ '.dsh-llm-hub-card__btn-group{display:flex;align-items:center;gap:5px;flex-wrap:wrap}',
377
+ '.dsh-llm-hub-card__status-group{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-left:auto}',
378
+ // 兼容旧 balance 容器类名
318
379
  '.dsh-llm-hub-balance{display:flex;align-items:center;gap:8px;flex-wrap:wrap;',
319
- 'margin-top:8px;padding:8px 10px;border-radius:8px;',
320
- 'border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);',
380
+ 'margin-top:4px;padding:4px 0;border:none;background:transparent;',
321
381
  'font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
322
- '.dsh-llm-hub-harness{display:flex;align-items:center;gap:8px;flex-wrap:wrap;',
323
- 'font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
324
- '.dsh-llm-hub-harness__chip{display:inline-flex;align-items:center;gap:5px;',
325
- 'padding:2px 8px;border-radius:999px;border:.5px solid var(--dsw-alias-border-l1);',
382
+ '.dsh-llm-hub-balance--stack{flex-direction:column;align-items:stretch;gap:6px}',
383
+ '.dsh-llm-hub-balance__facts{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}',
384
+ '.dsh-llm-hub-balance-label{font-size:12px;color:var(--dsw-alias-label-secondary);font-weight:500}',
385
+ '.dsh-llm-hub-amount{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);',
386
+ 'font-variant-numeric:tabular-nums;letter-spacing:-0.2px}',
387
+ '.dsh-llm-hub-amount.is-low{color:var(--dsw-alias-state-error-primary)}',
388
+ '.dsh-llm-hub-subtext{font-size:11px;color:var(--dsw-alias-label-secondary);opacity:.8}',
389
+ '.dsh-llm-hub-warn{color:var(--dsw-alias-state-warn-primary);font-size:11px}',
390
+ '.dsh-llm-hub-error{color:var(--dsw-alias-state-error-primary);font-size:11px}',
391
+ // 去掉粗糙灰底框,改用轻量排版标签
392
+ '.dsh-llm-hub-tag{display:inline-flex;align-items:center;gap:3px;padding:0 2px;',
393
+ 'border:none;background:transparent;',
394
+ 'color:var(--dsw-alias-label-secondary);font-size:11.5px;white-space:nowrap;line-height:16px}',
395
+ '.dsh-llm-hub-tag--muted{opacity:.65}',
396
+ '.dsh-llm-hub-warn-badge{display:inline-flex;align-items:center;gap:3px;padding:1px 5px;border-radius:3px;',
397
+ 'border:.5px solid var(--dsw-alias-state-warn-primary);background:rgba(234,179,8,.08);',
398
+ 'color:var(--dsw-alias-state-warn-primary);font-size:10.5px}',
399
+ // 现代 Ghost 微操作按钮:去硬框、低视觉干扰
400
+ '.dsh-llm-hub-btn{box-sizing:border-box;display:inline-flex;align-items:center;gap:3px;',
401
+ 'height:22px;padding:0 6px;font:inherit;font-size:11px;line-height:1;color:var(--dsw-alias-label-secondary);',
402
+ 'background:transparent;cursor:pointer;border:.5px solid var(--dsw-alias-border-l1);',
403
+ 'border-radius:4px;white-space:nowrap;transition:all .15s ease}',
404
+ '.dsh-llm-hub-btn:hover:not(:disabled){color:var(--dsw-alias-label-primary);',
405
+ 'background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l2)}',
406
+ '.dsh-llm-hub-btn:disabled{cursor:default;opacity:.45}',
407
+ // API Key 徽章:透明极简底色,精致内联 SVG
408
+ '.dsh-llm-hub-key-badge{box-sizing:border-box;display:inline-flex;align-items:center;',
409
+ 'height:20px;padding:0 3px 0 6px;gap:3px;border-radius:4px;flex:0 0 auto;',
410
+ 'border:.5px solid var(--dsw-alias-border-l1);background:transparent;',
411
+ 'color:var(--dsw-alias-label-secondary);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;',
412
+ 'font-size:10.5px;cursor:pointer;user-select:none;transition:all .15s ease;max-width:260px}',
413
+ '.dsh-llm-hub-quota-bar{display:flex;align-items:center;gap:6px;flex-wrap:wrap}',
414
+ '.dsh-llm-hub-key-badge:hover{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);',
326
415
  'background:var(--dsw-alias-bg-layer-2)}',
327
- '.dsh-llm-hub-harness__chip--missing{opacity:.55}',
328
- '.dsh-llm-hub-harness__dot{width:6px;height:6px;border-radius:50%;',
416
+ '.dsh-llm-hub-key-badge.is-copied{border-color:var(--dsw-alias-state-success-primary,#10b981);',
417
+ 'color:var(--dsw-alias-state-success-primary,#10b981);background:rgba(16,185,129,.08)}',
418
+ '.dsh-llm-hub-key-badge__text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1}',
419
+ '.dsh-llm-hub-key-badge__copied{display:inline-flex;align-items:center;gap:3px;font-weight:500}',
420
+ '.dsh-llm-hub-key-badge__btn{display:inline-flex;align-items:center;justify-content:center;',
421
+ 'width:16px;height:16px;padding:0;border:0;background:0 0;color:inherit;cursor:pointer;',
422
+ 'border-radius:3px;opacity:.65;transition:all .12s ease;flex:none}',
423
+ '.dsh-llm-hub-key-badge__btn:hover{opacity:1;background:var(--dsw-alias-interactive-bg-hover)}',
424
+ '.dsh-llm-hub-key-badge__btn.is-active{opacity:1;color:var(--dsw-alias-brand-primary)}',
425
+ // 直接融入 Provider 行的健康与延迟微徽章
426
+ '.dsh-llm-hub-health-badge{display:inline-flex;align-items:center;gap:4.5px;font-size:11px;',
427
+ 'font-variant-numeric:tabular-nums;line-height:1;color:var(--dsw-alias-label-secondary)}',
428
+ '.dsh-llm-hub-health-badge.is-ok{color:var(--dsw-alias-state-success-primary,#10b981)}',
429
+ '.dsh-llm-hub-health-badge.is-err{color:var(--dsw-alias-state-error-primary,#ef4444)}',
430
+ '.dsh-llm-hub-health-dot{width:5px;height:5px;border-radius:50%;flex:none}',
431
+ '.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)}',
432
+ '.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)}',
433
+ // 状态指示条与小圆点(保留以兼容历史与测试)
434
+ '.dsh-llm-hub-status-text{display:inline-flex;align-items:center;gap:5px;font-size:11px;',
435
+ 'color:var(--dsw-alias-label-secondary)}',
436
+ '.dsh-llm-hub-status-text.is-err{color:var(--dsw-alias-state-error-primary)}',
437
+ '.dsh-llm-hub-status-dot{width:6px;height:6px;border-radius:50%;background:var(--dsw-alias-label-secondary);flex:none}',
438
+ '.dsh-llm-hub-status-dot.is-ok{background:var(--dsw-alias-state-success-primary,#10b981);',
439
+ 'box-shadow:0 0 5px rgba(16,185,129,.5)}',
440
+ '.dsh-llm-hub-status-dot.is-err{background:var(--dsw-alias-state-error-primary);',
441
+ 'box-shadow:0 0 5px rgba(239,68,68,.5)}',
442
+ '.dsh-llm-hub-dot-sep{opacity:.35;margin:0 1px}',
443
+ // harness
444
+ '.dsh-llm-hub-harness{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:6px;',
445
+ 'font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary)}',
446
+ '.dsh-llm-hub-harness__chip{display:inline-flex;align-items:center;gap:4px;',
447
+ 'padding:2px 7px;border-radius:999px;border:.5px solid var(--dsw-alias-border-l1);',
448
+ 'background:transparent}',
449
+ '.dsh-llm-hub-harness__chip--missing{opacity:.45}',
450
+ '.dsh-llm-hub-harness__dot{width:5px;height:5px;border-radius:50%;',
329
451
  'background:var(--dsw-alias-label-secondary);flex:none}',
330
- '.dsh-llm-hub-harness__dot--ready{background:var(--dsw-alias-state-success-primary,#3fb950)}',
452
+ '.dsh-llm-hub-harness__dot--ready{background:var(--dsw-alias-state-success-primary,#10b981)}',
331
453
  '.dsh-llm-hub-harness__name{color:var(--dsw-alias-label-primary)}',
332
454
  '.dsh-llm-hub-harness__state{opacity:.75}',
333
- '.dsh-llm-hub-balance__label{color:var(--dsw-alias-label-secondary)}',
334
- '.dsh-llm-hub-balance__amount{color:var(--dsw-alias-label-primary);font-weight:600;',
335
- 'font-variant-numeric:tabular-nums}',
336
- '.dsh-llm-hub-balance__amount--low{color:var(--dsw-alias-state-error-primary)}',
337
- '.dsh-llm-hub-balance__breakdown{color:var(--dsw-alias-label-secondary);opacity:.8}',
338
- '.dsh-llm-hub-balance__warn{color:var(--dsw-alias-state-warn-primary)}',
339
- '.dsh-llm-hub-balance__error{color:var(--dsw-alias-state-error-primary)}',
340
- '.dsh-llm-hub-balance__spacer{flex:1 1 auto}',
341
- // 一张卡一个盒子:事实行 + 动作条。原来是两个盒子,白吃一行高度。
342
- '.dsh-llm-hub-balance--stack{flex-direction:column;align-items:stretch;gap:6px}',
343
- '.dsh-llm-hub-balance__facts{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}',
344
- '.dsh-llm-hub-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
345
- '.dsh-llm-hub-actions__button{box-sizing:border-box;height:24px;padding:0 10px;font:inherit;',
346
- 'font-size:12px;line-height:16px;color:var(--dsw-alias-label-primary);background:0 0;cursor:pointer;',
347
- 'border:.5px solid var(--dsw-alias-border-l3);border-radius:12px;white-space:nowrap}',
348
- '.dsh-llm-hub-actions__button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
349
- '.dsh-llm-hub-actions__button:disabled{cursor:default;opacity:.5}',
350
- // 目录选择器:沿用卡片本身的 token,不引入新配色
351
- '.dsh-llm-hub-picker{margin-top:6px;padding:8px 10px;border-radius:8px;',
455
+ // 目录选择器 Drawer
456
+ '.dsh-llm-hub-picker{margin-top:6px;padding:8px 10px;border-radius:6px;',
352
457
  'border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2)}',
353
458
  '.dsh-llm-hub-picker__head{display:flex;align-items:center;justify-content:space-between;',
354
- 'gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary);margin-bottom:6px}',
355
- '.dsh-llm-hub-picker__list{max-height:220px;overflow-y:auto;display:flex;',
459
+ 'gap:8px;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-bottom:6px}',
460
+ '.dsh-llm-hub-picker__list{max-height:200px;overflow-y:auto;display:flex;',
356
461
  'flex-direction:column;gap:2px}',
357
- '.dsh-llm-hub-picker__item{display:flex;align-items:center;gap:6px;font-size:12px;',
462
+ '.dsh-llm-hub-picker__item{display:flex;align-items:center;gap:6px;font-size:11.5px;',
358
463
  'line-height:20px;color:var(--dsw-alias-label-primary);cursor:pointer}',
359
464
  '.dsh-llm-hub-picker__id{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}',
360
- '.dsh-llm-hub-foot{display:flex;align-items:center;gap:12px;margin-top:10px;',
361
- 'padding:2px 2px;font-size:12px;color:var(--dsw-alias-label-secondary);opacity:.8}',
362
- '.dsh-llm-hub-foot__name{font-variant-numeric:tabular-nums}',
465
+ '.dsh-llm-hub-picker__tag{font-size:10.5px;padding:0 4px;border-radius:3px;',
466
+ 'color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-3)}',
467
+ // 页脚
468
+ '.dsh-llm-hub-foot{display:flex;align-items:center;gap:10px;margin-top:10px;',
469
+ 'padding:2px 2px;font-size:11px;color:var(--dsw-alias-label-secondary);opacity:.75}',
470
+ '.dsh-llm-hub-foot__name{font-variant-numeric:tabular-nums;opacity:.9}',
363
471
  '.dsh-llm-hub-foot__link{color:var(--dsw-alias-label-link);text-decoration:none;',
364
- 'border:0;background:0 0;padding:0;font:inherit;cursor:pointer}',
472
+ 'border:0;background:0 0;padding:0;font:inherit;font-size:11px;cursor:pointer}',
365
473
  '.dsh-llm-hub-foot__link:hover{text-decoration:underline}',
366
- '.dsh-llm-hub-picker__tag{font-size:11px;padding:0 5px;border-radius:4px;',
367
- 'color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-3)}',
368
- '.dsh-llm-hub-balance__button{border:0;background:0 0;padding:0;cursor:pointer;',
369
- 'font-size:12px;color:var(--dsw-alias-brand-primary)}',
370
- '.dsh-llm-hub-balance__button:disabled{cursor:default;opacity:.5}',
371
- '.dsh-llm-hub-balance__tag{padding:1px 6px;border-radius:6px;',
372
- 'border:.5px solid var(--dsw-alias-border-l1);',
373
- 'color:var(--dsw-alias-label-secondary);white-space:nowrap}',
374
- // 被摘掉的 provider:在它自己的卡片动作条里标一枚小片,不另开面板
375
- // (2026-09-16 owner:「这不是很多余吗,上面不都是显示了吗」)。
376
- '.dsh-llm-hub-chip{box-sizing:border-box;display:inline-flex;align-items:center;height:24px;padding:0 8px;',
377
- 'border-radius:12px;font-size:12px;line-height:16px;white-space:nowrap;',
378
- 'border:.5px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary)}',
474
+ '.dsh-llm-hub-chip{box-sizing:border-box;display:inline-flex;align-items:center;height:20px;padding:0 6px;',
475
+ 'border-radius:10px;font-size:10.5px;line-height:14px;white-space:nowrap;',
476
+ 'border:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary)}',
379
477
  '.dsh-llm-hub-chip--hidden{border-color:var(--dsw-alias-state-error-primary);',
380
478
  'color:var(--dsw-alias-state-error-primary)}',
381
479
  '.dsh-llm-hub-foot__hidden{color:var(--dsw-alias-state-error-primary)}',
382
- // 用量卡:放在 footer 之前(order 80 vs harness 90 vs footer 100)
480
+ // 用量卡:紧凑通透,不再沉重
383
481
  '.dsh-llm-hub-usage{display:flex;flex-direction:column;gap:6px;margin-top:10px;',
384
- 'padding:8px 10px;border-radius:8px;',
385
- 'border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);',
386
- 'font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
387
- '.dsh-llm-hub-usage__head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
482
+ 'padding:8px 10px;border-radius:6px;',
483
+ 'border:.5px solid var(--dsw-alias-border-l1);background:transparent;',
484
+ 'font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary)}',
485
+ '.dsh-llm-hub-usage__head{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}',
388
486
  '.dsh-llm-hub-usage__title{color:var(--dsw-alias-label-primary);font-weight:600}',
389
- '.dsh-llm-hub-usage__facts{display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
390
- '.dsh-llm-hub-usage__tag{display:inline-flex;align-items:center;gap:5px;padding:2px 8px;',
391
- 'border-radius:999px;border:.5px solid var(--dsw-alias-border-l1);',
392
- 'background:var(--dsw-alias-bg-layer-3)}',
487
+ '.dsh-llm-hub-usage__facts{display:flex;align-items:center;gap:6px;flex-wrap:wrap}',
488
+ '.dsh-llm-hub-usage__tag{display:inline-flex;align-items:center;gap:3px;padding:0 5px;',
489
+ 'border-radius:3px;border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);font-size:10.5px}',
393
490
  '.dsh-llm-hub-usage__top{display:flex;flex-direction:column;gap:2px;margin-top:2px}',
394
491
  '.dsh-llm-hub-usage__top-row{display:flex;justify-content:space-between;gap:8px;',
395
- 'color:var(--dsw-alias-label-primary)}',
492
+ 'color:var(--dsw-alias-label-primary);font-size:11px}',
396
493
  '.dsh-llm-hub-usage__top-row small{color:var(--dsw-alias-label-secondary);opacity:.8}',
397
- '.dsh-llm-hub-usage__actions{display:flex;gap:6px;flex-wrap:wrap}',
398
- // 健康看板:放在用量卡之上(order 70 vs usage 80)
399
- '.dsh-llm-hub-health{display:flex;flex-direction:column;gap:6px;margin-top:10px;',
400
- 'padding:8px 10px;border-radius:8px;',
401
- 'border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);',
402
- 'font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
403
- '.dsh-llm-hub-health__head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
404
- '.dsh-llm-hub-health__title{color:var(--dsw-alias-label-primary);font-weight:600}',
405
- '.dsh-llm-hub-health__rows{display:flex;flex-direction:column;gap:2px}',
406
- '.dsh-llm-hub-health__row{display:flex;align-items:center;gap:6px;flex-wrap:wrap;',
407
- 'padding:2px 4px;border-radius:6px}',
408
- '.dsh-llm-hub-health__name{color:var(--dsw-alias-label-primary);font-weight:600;',
409
- 'min-width:80px}',
410
- '.dsh-llm-hub-health__chip{display:inline-flex;align-items:center;height:20px;padding:0 8px;',
411
- 'border-radius:999px;border:.5px solid var(--dsw-alias-border-l3);',
412
- 'font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary)}',
413
- '.dsh-llm-hub-health__chip--available{border-color:var(--dsw-alias-state-success-primary,#3fb950);',
414
- 'color:var(--dsw-alias-state-success-primary,#3fb950)}',
415
- '.dsh-llm-hub-health__chip--unavailable{border-color:var(--dsw-alias-state-error-primary);',
416
- 'color:var(--dsw-alias-state-error-primary)}',
417
- '.dsh-llm-hub-health__chip--unknown{border-color:var(--dsw-alias-border-l3);',
418
- 'color:var(--dsw-alias-label-secondary)}',
419
- '.dsh-llm-hub-health__metric{font-size:11px;opacity:.85}',
420
- '.dsh-llm-hub-health__metric--time{margin-left:auto}',
421
- '.dsh-llm-hub-health__reason{color:var(--dsw-alias-state-warn-primary);',
422
- 'max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
423
- // 当前路由卡:放在 health 70 与 usage 80 之间(order 75)
494
+ '.dsh-llm-hub-usage__actions{display:flex;gap:10px;flex-wrap:wrap;padding-top:4px;border-top:.5px solid var(--dsw-alias-border-l1)}',
495
+ // 健康看板:样式保留以兼容
496
+ '.dsh-llm-hub-health{display:none}',
497
+ // 当前路由卡
424
498
  '.dsh-llm-hub-routing{display:flex;flex-direction:column;gap:6px;margin-top:10px;',
425
- 'padding:8px 10px;border-radius:8px;',
426
- 'border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);',
427
- 'font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
499
+ 'padding:8px 10px;border-radius:6px;',
500
+ 'border:.5px solid var(--dsw-alias-border-l1);background:transparent;',
501
+ 'font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary)}',
428
502
  '.dsh-llm-hub-routing__head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
429
503
  '.dsh-llm-hub-routing__title{color:var(--dsw-alias-label-primary);font-weight:600}',
430
504
  '.dsh-llm-hub-routing__active{color:var(--dsw-alias-label-primary)}',
431
505
  '.dsh-llm-hub-routing__rows{display:flex;flex-direction:column;gap:2px}',
432
- '.dsh-llm-hub-routing__row{display:flex;align-items:center;gap:6px;flex-wrap:wrap;',
433
- 'padding:2px 4px;border-radius:6px}',
434
- '.dsh-llm-hub-routing__role{display:inline-flex;align-items:center;height:20px;padding:0 8px;',
435
- 'border-radius:999px;border:.5px solid var(--dsw-alias-border-l3);',
436
- 'background:var(--dsw-alias-bg-layer-3);font-size:11px;color:var(--dsw-alias-label-secondary)}',
506
+ '.dsh-llm-hub-routing__row{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:2px 4px}',
507
+ '.dsh-llm-hub-routing__role{display:inline-flex;align-items:center;height:16px;padding:0 5px;',
508
+ 'border-radius:3px;border:.5px solid var(--dsw-alias-border-l2);',
509
+ 'background:transparent;font-size:10px;color:var(--dsw-alias-label-secondary)}',
437
510
  '.dsh-llm-hub-routing__name{color:var(--dsw-alias-label-primary);font-weight:600}',
438
- '.dsh-llm-hub-routing__suggestion{color:var(--dsw-alias-state-warn-primary);',
439
- 'padding-top:4px}',
440
- '.dsh-llm-hub-routing__reason{color:var(--dsw-alias-state-error-primary);',
441
- 'padding-top:4px}',
442
- // 多账号 key 轮换:放在 routing 75 之前(order 73)
511
+ '.dsh-llm-hub-routing__suggestion{color:var(--dsw-alias-state-warn-primary);padding-top:2px}',
512
+ '.dsh-llm-hub-routing__reason{color:var(--dsw-alias-state-error-primary);padding-top:2px}',
513
+ // 多账号 key 轮换
443
514
  '.dsh-llm-hub-keypool{display:flex;flex-direction:column;gap:6px;margin-top:10px;',
444
- 'padding:8px 10px;border-radius:8px;',
445
- 'border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);',
446
- 'font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
447
- '.dsh-llm-hub-keypool__head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
448
- '.dsh-llm-hub-keypool__title{color:var(--dsw-alias-label-primary);font-weight:600}',
449
- '.dsh-llm-hub-keypool__rows{display:flex;flex-direction:column;gap:2px}',
450
- '.dsh-llm-hub-keypool__row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;',
451
- 'padding:2px 4px;border-radius:6px}',
452
- '.dsh-llm-hub-keypool__name{color:var(--dsw-alias-label-primary);font-weight:600;min-width:120px}',
453
- '.dsh-llm-hub-keypool__meta{font-size:11px;opacity:.7}',
515
+ 'padding:8px 10px;border-radius:6px;',
516
+ 'border:.5px solid var(--dsw-alias-border-l1);background:transparent;',
517
+ 'font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary)}',
518
+ '.dsh-llm-hub-keypool__head{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}',
519
+ '.dsh-llm-hub-keypool__title{color:var(--dsw-alias-label-primary);font-weight:600;display:inline-flex;align-items:center;gap:5px}',
520
+ '.dsh-llm-hub-keypool__rows{display:flex;flex-direction:column;gap:3px}',
521
+ '.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)}',
522
+ '.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)}',
523
+ '.dsh-llm-hub-keypool__name{color:var(--dsw-alias-label-primary);font-weight:600;min-width:120px;font-family:ui-monospace,SFMono-Regular,monospace}',
524
+ '.dsh-llm-hub-keypool__meta{font-size:10.5px;opacity:.7}',
454
525
  '.dsh-llm-hub-keypool__chosen{display:flex;align-items:baseline;gap:4px;',
455
526
  'color:var(--dsw-alias-label-primary)}',
456
- '.dsh-llm-hub-keypool__chosen small{color:var(--dsw-alias-label-secondary);opacity:.75;font-size:11px}',
527
+ '.dsh-llm-hub-keypool__chosen small{color:var(--dsw-alias-label-secondary);opacity:.75;font-size:10.5px}',
457
528
  '.dsh-llm-hub-keypool__pending{color:var(--dsw-alias-label-secondary);opacity:.6}',
529
+ // Hero 全景统计大横条
530
+ '.dsh-llm-hub-hero{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:6px;margin:12px 0 6px 0}',
531
+ '.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)}',
532
+ '.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}',
533
+ '.dsh-llm-hub-hero__label{font-size:10.5px;color:var(--dsw-alias-label-secondary);opacity:.85}',
534
+ // 热门大模型一键装配 Hub
535
+ '.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}',
536
+ '.dsh-llm-hub-presets__head{display:flex;align-items:center;justify-content:space-between;gap:8px;cursor:pointer;user-select:none}',
537
+ '.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}',
538
+ '.dsh-llm-hub-presets__grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:6px;margin-top:6px}',
539
+ '.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}',
540
+ '.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))}',
541
+ '.dsh-llm-hub-presets__name{font-weight:600;font-size:11.5px;color:var(--dsw-alias-label-primary)}',
542
+ '.dsh-llm-hub-presets__desc{font-size:10px;color:var(--dsw-alias-label-secondary);opacity:.75;line-height:1.3}',
543
+ '.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}',
544
+ '.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}',
545
+ // 延迟高亮胶囊
546
+ '.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}',
547
+ '.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)}',
548
+ '.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)}',
549
+ '.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)}',
550
+ // Webkubor 插件全家桶互导矩阵
551
+ '.dsh-suite-dock{margin-top:14px;padding:14px 16px;background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,0.03));border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,0.08));border-radius:10px;backdrop-filter:blur(10px)}',
552
+ '.dsh-suite-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;flex-wrap:wrap;gap:8px}',
553
+ '.dsh-suite-title{font-size:12px;font-weight:650;color:var(--dsw-alias-label-primary);margin:0;display:flex;align-items:center;gap:6px}',
554
+ '.dsh-suite-desc{font-size:10.5px;color:var(--dsw-alias-label-tertiary);letter-spacing:.2px}',
555
+ '.dsh-suite-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px}',
556
+ '.dsh-suite-card{display:flex;flex-direction:column;justify-content:space-between;padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-layer-3,rgba(255,255,255,0.02));border:1px solid var(--dsw-alias-border-l2,rgba(255,255,255,0.05));transition:all .2s cubic-bezier(0.16,1,0.3,1)}',
557
+ '.dsh-suite-card:hover{transform:translateY(-1px);border-color:var(--dsw-alias-brand-primary,#3b82f6);box-shadow:0 4px 14px rgba(0,0,0,0.15)}',
558
+ '.dsh-suite-card-top{display:flex;align-items:center;gap:6px;margin-bottom:4px}',
559
+ '.dsh-suite-card-icon{font-size:15px;line-height:1}',
560
+ '.dsh-suite-card-name{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);text-decoration:none}',
561
+ '.dsh-suite-card-desc{font-size:10.5px;color:var(--dsw-alias-label-secondary);line-height:1.4;margin-bottom:8px;flex:1}',
562
+ '.dsh-suite-card-bottom{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-top:auto;font-size:10.5px}',
563
+ '.dsh-suite-badge-active{display:inline-flex;align-items:center;gap:4px;color:var(--dsw-alias-state-success-primary,#10b981);font-weight:600;font-size:10.5px}',
564
+ '.dsh-suite-badge-active-dot{width:5px;height:5px;border-radius:50%;background:#10b981;box-shadow:0 0 6px rgba(16,185,129,0.6)}',
565
+ '.dsh-suite-btn-action{cursor:pointer;padding:2px 8px;border-radius:4px;font-size:10px;font-weight:550;background:var(--dsw-alias-interactive-bg-subtle,rgba(255,255,255,0.06));border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);transition:all .15s;text-decoration:none;display:inline-flex;align-items:center;gap:3px}',
566
+ '.dsh-suite-btn-action:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,0.12));border-color:var(--dsw-alias-border-l1)}',
567
+ '.dsh-suite-link{color:var(--dsw-alias-label-tertiary);text-decoration:none;font-size:10px;display:inline-flex;align-items:center}',
568
+ '.dsh-suite-link:hover{color:var(--dsw-alias-label-primary)}'
458
569
  ].join('')
459
570
  document.head.appendChild(style)
460
571
  }
461
572
 
573
+ /** 矢量 SVG 线条图标集合(彻底杜绝系统 emoji) */
574
+ function EyeIcon() {
575
+ return h('svg', { width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round' },
576
+ h('path', { d: 'M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z' }),
577
+ h('circle', { cx: 12, cy: 12, r: 3 })
578
+ )
579
+ }
580
+ function EyeOffIcon() {
581
+ return h('svg', { width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round' },
582
+ h('path', { d: 'M9.88 9.88a3 3 0 1 0 4.24 4.24' }),
583
+ 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' }),
584
+ 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' }),
585
+ h('line', { x1: 2, y1: 2, x2: 22, y2: 22 })
586
+ )
587
+ }
588
+ function CopyIcon() {
589
+ return h('svg', { width: 11, height: 11, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round' },
590
+ h('rect', { width: 14, height: 14, x: 8, y: 8, rx: 2, ry: 2 }),
591
+ h('path', { d: 'M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2' })
592
+ )
593
+ }
594
+ function CheckIcon() {
595
+ return h('svg', { width: 11, height: 11, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2.2, strokeLinecap: 'round', strokeLinejoin: 'round' },
596
+ h('polyline', { points: '20 6 9 17 4 12' })
597
+ )
598
+ }
599
+ function RefreshIcon(props) {
600
+ 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 },
601
+ h('path', { d: 'M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8' }),
602
+ h('path', { d: 'M3 3v5h5' }),
603
+ h('path', { d: 'M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16' }),
604
+ h('path', { d: 'M16 21h5v-5' })
605
+ )
606
+ }
607
+
608
+ /**
609
+ * API Key 脱敏展示与一键复制徽章。
610
+ * - 默认展示首尾可见的脱敏串(如 mgk_li····8cV9)
611
+ * - 矢量 Eye 图标切换查看/收起完整明文
612
+ * - 点击徽章或 Copy 图标一键复制完整 Key,并有绿色无缝状态反馈
613
+ */
614
+ function ApiKeyBadge(props) {
615
+ const apiKey = typeof props.apiKey === 'string' && props.apiKey.trim() !== '' ? props.apiKey.trim() : null
616
+ const keyMasked = typeof props.keyMasked === 'string' && props.keyMasked.trim() !== '' ? props.keyMasked.trim() : null
617
+ const t = typeof props.t === 'function' ? props.t : fallbackT
618
+ const [revealed, setRevealed] = React.useState(false)
619
+ const [copied, setCopied] = React.useState(false)
620
+
621
+ if (apiKey === null && keyMasked === null) {
622
+ return h('span', { className: 'dsh-llm-hub-tag dsh-llm-hub-tag--muted' }, t('keyMissing'))
623
+ }
624
+
625
+ const rawDisplay = revealed ? (apiKey ?? keyMasked) : (keyMasked ?? '••••••••')
626
+ const display = rawDisplay.replace(/•+/g, '····')
627
+
628
+ const copyKey = (e) => {
629
+ e.stopPropagation()
630
+ const textToCopy = apiKey ?? keyMasked
631
+ if (!textToCopy) return
632
+ navigator.clipboard?.writeText(textToCopy)
633
+ .then(() => {
634
+ setCopied(true)
635
+ setTimeout(() => setCopied(false), 1600)
636
+ })
637
+ .catch(() => {})
638
+ }
639
+
640
+ const toggleReveal = (e) => {
641
+ e.stopPropagation()
642
+ if (apiKey === null) return
643
+ setRevealed((prev) => !prev)
644
+ }
645
+
646
+ return h('div', {
647
+ className: 'dsh-llm-hub-key-badge' + (copied ? ' is-copied' : ''),
648
+ title: copied ? t('copiedKey') : (apiKey === null ? t('showKeyUnavailable') : (revealed ? t('hideKeyHint') : t('showKeyHint'))),
649
+ onClick: copyKey
650
+ },
651
+ copied
652
+ ? h('span', { className: 'dsh-llm-hub-key-badge__copied' },
653
+ h(CheckIcon, null),
654
+ h('span', null, t('copiedKey'))
655
+ )
656
+ : h('span', { className: 'dsh-llm-hub-key-badge__text' }, display),
657
+ !copied && apiKey !== null ? h('button', {
658
+ type: 'button',
659
+ className: 'dsh-llm-hub-key-badge__btn' + (revealed ? ' is-active' : ''),
660
+ title: revealed ? t('hideKey') : t('showKey'),
661
+ onClick: toggleReveal
662
+ }, revealed ? h(EyeOffIcon, null) : h(EyeIcon, null)) : null,
663
+ !copied ? h('button', {
664
+ type: 'button',
665
+ className: 'dsh-llm-hub-key-badge__btn',
666
+ title: t('copiedKey'),
667
+ onClick: copyKey
668
+ }, h(CopyIcon, null)) : null
669
+ )
670
+ }
671
+
462
672
  /**
463
673
  * 余额卡:挂载即查,可手动刷新。
464
674
  * @param props - owner props(provider/configured/keyConfigured)+ 注入的 `t`。
@@ -469,6 +679,7 @@ window.__ModuleLoader__.load({
469
679
  const keyConfigured = props.keyConfigured !== false
470
680
  const [state, setState] = React.useState({ status: 'idle' })
471
681
  const [warning, setWarning] = React.useState(null)
682
+ const [isRefreshing, setIsRefreshing] = React.useState(false)
472
683
  // 官方直连也可能被判不可用(key 被拒 / 账户欠费)。同一个状态片,一致的处理。
473
684
  const availability = props.availability
474
685
  const availabilityState = availability === undefined
@@ -477,14 +688,17 @@ window.__ModuleLoader__.load({
477
688
  const verdict = Array.isArray(availabilityState.providers)
478
689
  ? availabilityState.providers.find((entry) => entry.provider === DEEPSEEK_PROVIDER)
479
690
  : undefined
480
- const hiddenFromDropdown = verdict !== undefined && verdict.state === 'unavailable'
691
+ const hiddenFromDropdown = Array.isArray(availabilityState.hidden)
692
+ ? availabilityState.hidden.includes(DEEPSEEK_PROVIDER)
693
+ : (verdict !== undefined && verdict.state === 'unavailable')
481
694
 
482
695
  const load = React.useCallback(() => {
483
696
  if (!keyConfigured) {
484
697
  setState({ status: 'nokey' })
485
698
  return
486
699
  }
487
- setState({ status: 'loading' })
700
+ setState((prev) => (prev.status === 'ok' ? prev : { status: 'loading' }))
701
+ setIsRefreshing(true)
488
702
  fetch(BALANCE_URL, { headers: { accept: 'application/json' } })
489
703
  .then(async (response) => {
490
704
  let body
@@ -496,59 +710,31 @@ window.__ModuleLoader__.load({
496
710
  if (body == null || body.ok !== true) {
497
711
  throw new Error(typeof body?.error === 'string' ? body.error : `HTTP ${response.status}`)
498
712
  }
499
- setState({ status: 'ok', isAvailable: body.isAvailable === true, balances: Array.isArray(body.balances) ? body.balances : [] })
713
+ setState({
714
+ status: 'ok',
715
+ isAvailable: body.isAvailable === true,
716
+ balances: Array.isArray(body.balances) ? body.balances : [],
717
+ apiKey: typeof body.apiKey === 'string' ? body.apiKey : undefined,
718
+ keyMasked: typeof body.keyMasked === 'string' ? body.keyMasked : undefined
719
+ })
500
720
  })
501
721
  .catch((error) => {
502
722
  setState({ status: 'error', error: error instanceof Error ? error.message : String(error) })
503
723
  })
724
+ .finally(() => {
725
+ setIsRefreshing(false)
726
+ })
504
727
  }, [keyConfigured])
505
728
 
506
729
  React.useEffect(() => {
507
730
  load()
508
731
  }, [load])
509
732
 
510
- const children = []
511
- children.push(h('span', { key: 'label', className: 'dsh-llm-hub-balance__label' }, t('balance')))
512
- if (hiddenFromDropdown) {
513
- children.push(h('span', {
514
- key: 'hidden-chip',
515
- className: 'dsh-llm-hub-chip dsh-llm-hub-chip--hidden',
516
- title: typeof verdict.reason === 'string' ? verdict.reason : ''
517
- }, t('availabilityUnavailable')))
518
- }
519
-
520
- if (state.status === 'nokey') {
521
- children.push(h('span', { key: 'nokey', className: 'dsh-llm-hub-balance__warn' }, t('noKey')))
522
- children.push(h('span', { key: 'hint', className: 'dsh-llm-hub-balance__breakdown' }, t('noKeyHint')))
523
- return h('div', { className: 'dsh-llm-hub-balance' }, children)
524
- }
525
-
526
- if (state.status === 'loading' || state.status === 'idle') {
527
- children.push(h('span', { key: 'loading', className: 'dsh-llm-hub-balance__breakdown' }, t('loading')))
528
- return h('div', { className: 'dsh-llm-hub-balance' }, children)
529
- }
530
-
531
- if (state.status === 'error') {
532
- children.push(h('span', { key: 'error', className: 'dsh-llm-hub-balance__error' }, `${t('failed')}: ${state.error}`))
533
- children.push(h('span', { key: 'spacer', className: 'dsh-llm-hub-balance__spacer' }))
534
- children.push(h('button', {
535
- key: 'retry',
536
- type: 'button',
537
- className: 'dsh-llm-hub-balance__button',
538
- onClick: load
539
- }, t('retry')))
540
- return h('div', { className: 'dsh-llm-hub-balance' }, children)
541
- }
733
+ const primary = state.status === 'ok' && Array.isArray(state.balances) ? state.balances[0] : undefined
542
734
 
543
- // status === 'ok'
544
- const primary = state.balances[0]
545
- if (primary === undefined) {
546
- children.push(h('span', { key: 'empty', className: 'dsh-llm-hub-balance__breakdown' }, t('loading')))
547
- return h('div', { className: 'dsh-llm-hub-balance' }, children)
548
- }
549
- // 余额预警:拿到数字后顺手问 host 是否低于阈值。状态独立:
550
- // warning=null 表示还没回(不显示标签)/ level='low' 才上色 + 提示。
735
+ // 余额预警:无条件在最外层声明 Hook,遵从 Rules of Hooks!
551
736
  React.useEffect(() => {
737
+ if (!primary || typeof primary.total !== 'string') return
552
738
  fetch(WARNING_URL, {
553
739
  method: 'POST',
554
740
  headers: { 'content-type': 'application/json', accept: 'application/json' },
@@ -566,35 +752,85 @@ window.__ModuleLoader__.load({
566
752
  }
567
753
  })
568
754
  .catch(() => setWarning(null))
569
- }, [primary.total, primary.currency])
570
- const symbol = primary.currency === 'CNY' ? '¥' : primary.currency === 'USD' ? '$' : `${primary.currency} `
571
- const amountClass = warning !== null && warning.level === 'low'
572
- ? 'dsh-llm-hub-balance__amount dsh-llm-hub-balance__amount--low'
573
- : 'dsh-llm-hub-balance__amount'
574
- children.push(h('span', { key: 'amount', className: amountClass }, `${symbol}${primary.total}`))
575
- if (warning !== null && warning.level === 'low') {
576
- children.push(h('span', {
577
- key: 'warning',
578
- className: 'dsh-llm-hub-balance__warn',
579
- title: warning.text
580
- }, '⚠️'))
755
+ }, [primary?.total, primary?.currency])
756
+
757
+ if (state.status === 'nokey') {
758
+ return h('div', { className: 'dsh-llm-hub-card' },
759
+ h('div', { className: 'dsh-llm-hub-card__meta' },
760
+ h('span', { className: 'dsh-llm-hub-warn' }, t('noKey')),
761
+ h('span', { className: 'dsh-llm-hub-subtext' }, t('noKeyHint'))
762
+ )
763
+ )
581
764
  }
582
- if (!state.isAvailable) children.push(h('span', { key: 'unavail', className: 'dsh-llm-hub-balance__warn' }, t('unavailable')))
765
+
766
+ if (state.status === 'loading' || state.status === 'idle') {
767
+ return h('div', { className: 'dsh-llm-hub-card' },
768
+ h('div', { className: 'dsh-llm-hub-card__meta' },
769
+ h('span', { className: 'dsh-llm-hub-subtext' }, t('loading'))
770
+ )
771
+ )
772
+ }
773
+
774
+ if (state.status === 'error') {
775
+ return h('div', { className: 'dsh-llm-hub-card' },
776
+ h('div', { className: 'dsh-llm-hub-card__meta' },
777
+ h('span', { className: 'dsh-llm-hub-error' }, `${t('failed')}: ${state.error}`),
778
+ h('button', {
779
+ type: 'button',
780
+ className: 'dsh-llm-hub-btn',
781
+ onClick: load
782
+ }, h(RefreshIcon, { spinning: isRefreshing }), t('retry'))
783
+ )
784
+ )
785
+ }
786
+
787
+ if (!primary) {
788
+ return h('div', { className: 'dsh-llm-hub-card' },
789
+ h('div', { className: 'dsh-llm-hub-card__meta' },
790
+ h('span', { className: 'dsh-llm-hub-subtext' }, t('loading'))
791
+ )
792
+ )
793
+ }
794
+
795
+ const symbol = primary.currency === 'CNY' ? '¥' : primary.currency === 'USD' ? '$' : `${primary.currency} `
583
796
  const breakdown = []
584
797
  if (primary.granted !== '0' && primary.granted !== '0.00') breakdown.push(`${t('granted')} ${symbol}${primary.granted}`)
585
798
  if (primary.toppedUp !== '0' && primary.toppedUp !== '0.00') breakdown.push(`${t('toppedUp')} ${symbol}${primary.toppedUp}`)
586
- if (breakdown.length > 0) {
587
- children.push(h('span', { key: 'breakdown', className: 'dsh-llm-hub-balance__breakdown' }, breakdown.join(' · ')))
588
- }
589
- children.push(h('span', { key: 'spacer', className: 'dsh-llm-hub-balance__spacer' }))
590
- children.push(h('button', {
591
- key: 'refresh',
592
- type: 'button',
593
- className: 'dsh-llm-hub-balance__button',
594
- title: t('refresh'),
595
- onClick: load
596
- }, t('refresh')))
597
- return h('div', { className: 'dsh-llm-hub-balance' }, children)
799
+
800
+ return h('div', { className: 'dsh-llm-hub-card' },
801
+ h('div', { className: 'dsh-llm-hub-card__meta' },
802
+ h('div', { className: 'dsh-llm-hub-card__tags' },
803
+ h('span', { className: 'dsh-llm-hub-balance-label' }, t('balance')),
804
+ h('span', { className: 'dsh-llm-hub-amount' + (warning?.level === 'low' ? ' is-low' : '') }, `${symbol}${primary.total}`),
805
+ h('span', { className: 'dsh-llm-hub-meta-sep' }, '·'),
806
+ h('span', { className: 'dsh-llm-hub-meta-text' }, '官方直连'),
807
+ h('span', { className: 'dsh-llm-hub-meta-sep' }, '·'),
808
+ h('span', {
809
+ className: 'dsh-llm-hub-health-badge ' + (state.isAvailable ? 'is-ok' : 'is-err'),
810
+ title: state.isAvailable ? '官方接口连通正常' : t('unavailable')
811
+ },
812
+ h('span', { className: 'dsh-llm-hub-health-dot ' + (state.isAvailable ? 'is-ok' : 'is-err') }),
813
+ state.isAvailable ? '连通正常' : t('unavailable')
814
+ ),
815
+ breakdown.length > 0 ? h('span', { className: 'dsh-llm-hub-subtext' }, `(${breakdown.join(' · ')})`) : null,
816
+ hiddenFromDropdown ? h('span', { className: 'dsh-llm-hub-chip dsh-llm-hub-chip--hidden' }, t('availabilityUnavailable')) : null
817
+ ),
818
+ h('div', { className: 'dsh-llm-hub-card__btn-group' },
819
+ h(ApiKeyBadge, {
820
+ apiKey: state.apiKey,
821
+ keyMasked: state.keyMasked,
822
+ t
823
+ }),
824
+ h('button', {
825
+ type: 'button',
826
+ className: 'dsh-llm-hub-btn',
827
+ disabled: isRefreshing,
828
+ title: t('refresh'),
829
+ onClick: load
830
+ }, h(RefreshIcon, { spinning: isRefreshing }))
831
+ )
832
+ )
833
+ )
598
834
  }
599
835
 
600
836
  /**
@@ -676,7 +912,9 @@ window.__ModuleLoader__.load({
676
912
  const verdict = Array.isArray(availabilityState.providers)
677
913
  ? availabilityState.providers.find((entry) => entry.provider === provider)
678
914
  : undefined
679
- const hiddenFromDropdown = verdict !== undefined && verdict.state === 'unavailable'
915
+ const hiddenFromDropdown = Array.isArray(availabilityState.hidden)
916
+ ? availabilityState.hidden.includes(provider)
917
+ : (verdict !== undefined && verdict.state === 'unavailable')
680
918
 
681
919
  React.useEffect(() => {
682
920
  if (provider === '' || status === null || status.ok !== true || !status.balanceAdapter) return undefined
@@ -778,90 +1016,108 @@ window.__ModuleLoader__.load({
778
1016
  // 2026-09-15 owner 报「为啥都显示状态读取失败」—— 页面一打开、或 DSH 刚重启
779
1017
  // 导致这次 fetch 断掉时,三行全是这句,看着像插件坏了,其实只是还没加载完。
780
1018
  if (status === null) {
781
- return h('div', { className: 'dsh-llm-hub-balance' },
782
- h('span', { className: 'dsh-llm-hub-balance__label' }, 'pi-ai'),
783
- h('span', { className: 'dsh-llm-hub-balance__breakdown' }, t('statusLoading')))
1019
+ return h('div', { className: 'dsh-llm-hub-card' },
1020
+ h('div', { className: 'dsh-llm-hub-card__meta' },
1021
+ h('span', { className: 'dsh-llm-hub-subtext' }, t('statusLoading'))))
784
1022
  }
785
1023
  if (status.ok !== true) {
786
- return h('div', { className: 'dsh-llm-hub-balance' },
787
- h('span', { className: 'dsh-llm-hub-balance__label' }, 'pi-ai'),
788
- h('span', { className: 'dsh-llm-hub-balance__breakdown' }, typeof status.error === 'string' ? status.error : t('statusFail')))
1024
+ return h('div', { className: 'dsh-llm-hub-card' },
1025
+ h('div', { className: 'dsh-llm-hub-card__meta' },
1026
+ h('span', { className: 'dsh-llm-hub-error' }, typeof status.error === 'string' ? status.error : t('statusFail'))))
789
1027
  }
790
1028
 
791
- // 事实行:一行装得下就一行,装不下让它自己换行。
792
- const facts = []
793
- facts.push(h('span', { key: 'label', className: 'dsh-llm-hub-balance__label' }, 'pi-ai'))
794
- facts.push(h('span', { key: 'name', className: 'dsh-llm-hub-balance__amount' }, status.displayName))
795
- facts.push(h('span', { key: 'count', className: 'dsh-llm-hub-balance__tag' }, `${t('configuredModels')} ${status.modelCount} ${t('modelsUnit')}`))
796
- facts.push(h('span', { key: 'key', className: 'dsh-llm-hub-balance__tag' }, status.keyConfigured === true ? 'Key ✓' : t('keyMissing')))
797
- // 协议与接入地址(2026-09-14 owner:「接入的协议和 apiurl 也需要可以看」)。
798
- // 这两项决定了这个 provider 到底连去哪、用哪套报文;出问题时第一眼要看的就是它们,
799
- // 之前只能去翻 settings.yaml。地址只显示域名,完整 URL 放 title —— 一行放不下,
800
- // 而域名已经够回答「连的是不是我以为的那个网关」。
1029
+ // 元信息标签组(不重复展示外层已有的大标题,只呈现核心事实)
1030
+ const tags = []
1031
+ tags.push(h('span', { key: 'count', className: 'dsh-llm-hub-tag' }, `${t('configuredModels')} ${status.modelCount} ${t('modelsUnit')}`))
801
1032
  if (typeof status.api === 'string' && status.api !== '') {
802
- facts.push(h('span', { key: 'api', className: 'dsh-llm-hub-balance__tag' }, `${t('protocol')} ${status.api}`))
1033
+ const shortApi = status.api.replace('-messages', '').replace('-completions', '')
1034
+ tags.push(h('span', { key: 'api', className: 'dsh-llm-hub-tag', title: `${t('protocol')}: ${status.api}` }, shortApi))
803
1035
  }
804
1036
  if (typeof status.baseURL === 'string' && status.baseURL !== '') {
805
1037
  let host = status.baseURL
806
1038
  try { host = new URL(status.baseURL).host } catch {}
807
- facts.push(h('span', {
1039
+ tags.push(h('span', { key: 'sep2', className: 'dsh-llm-hub-meta-sep' }, '·'))
1040
+ tags.push(h('span', {
808
1041
  key: 'base',
809
- className: 'dsh-llm-hub-balance__tag',
1042
+ className: 'dsh-llm-hub-meta-text',
810
1043
  title: `${t('endpoint')}: ${status.baseURL}`
811
1044
  }, host))
812
1045
  }
813
- const balanceText = renderBalance(balance, t)
814
- if (balanceText !== null) facts.push(h('span', { key: 'bal', className: 'dsh-llm-hub-balance__tag' }, balanceText))
815
- if (warning !== null && warning.level === 'low') {
816
- facts.push(h('span', {
817
- key: 'warning',
818
- className: 'dsh-llm-hub-balance__warn',
819
- title: warning.text
820
- }, '⚠️'))
1046
+
1047
+ // 健康与延迟直接融入 Provider 行(消除冗余下方看板)
1048
+ const probing = probe !== null && probe.phase === 'loading'
1049
+ const probeDone = probe !== null && probe.phase === 'done'
1050
+ const latency = probeDone && typeof probe.body?.latencyMs === 'number'
1051
+ ? probe.body.latencyMs
1052
+ : (typeof verdict?.latencyMs === 'number' ? verdict.latencyMs : null)
1053
+ const isReachable = probeDone
1054
+ ? (probe.body?.ok === true && probe.body?.reachable === true)
1055
+ : (verdict === undefined || verdict.state !== 'unavailable')
1056
+ const errMsg = probeDone
1057
+ ? (probe.body?.error ?? '')
1058
+ : (verdict?.state === 'unavailable' ? (verdict.reason ?? t('availabilityUnavailable')) : '')
1059
+
1060
+ tags.push(h('span', { key: 'sep3', className: 'dsh-llm-hub-meta-sep' }, '·'))
1061
+ if (probing) {
1062
+ tags.push(h('span', { key: 'health', className: 'dsh-llm-hub-health-badge' }, h(RefreshIcon, { spinning: true }), t('probing')))
1063
+ } else if (isReachable) {
1064
+ const latencyClass = latency === null ? 'is-fast' : (latency < 300 ? 'is-fast' : (latency < 800 ? 'is-normal' : 'is-slow'))
1065
+ tags.push(h('span', {
1066
+ key: 'health',
1067
+ className: 'dsh-llm-hub-health-badge is-ok',
1068
+ title: latency !== null ? `连通正常 · 延迟 ${latency}ms` : '连通正常'
1069
+ },
1070
+ h('span', { className: 'dsh-llm-hub-health-dot is-ok' }),
1071
+ latency !== null
1072
+ ? h('span', { className: `dsh-llm-hub-latency-badge ${latencyClass}` }, `${latency}ms`)
1073
+ : '正常'
1074
+ ))
1075
+ } else {
1076
+ tags.push(h('span', {
1077
+ key: 'health',
1078
+ className: 'dsh-llm-hub-health-badge is-err dsh-llm-hub-chip--hidden',
1079
+ title: errMsg || t('availabilityUnavailable')
1080
+ },
1081
+ h('span', { className: 'dsh-llm-hub-health-dot is-err' }),
1082
+ t('availabilityUnavailable')
1083
+ ))
821
1084
  }
822
1085
 
823
- // 动作行:**所有按钮挤在同一行**。
824
- //
825
- // 2026-09-16 owner 指着截图:「UI 是不是优化下,有点浪费空间,有的就是一个文字占一行」。
826
- // 原来是两个各自带边框+内边距的盒子:事实行末尾挂「探测网关」(宽度不够就换行),
827
- // 再另起一盒放「拉取目录」。一张卡白吃两行多。现在合成一个盒子、按钮排成一条工具条。
828
- const actions = []
829
- if (hiddenFromDropdown) {
830
- actions.push(h('span', {
831
- key: 'hidden-chip',
832
- className: 'dsh-llm-hub-chip dsh-llm-hub-chip--hidden',
833
- title: typeof verdict.reason === 'string' ? verdict.reason : ''
834
- }, t('availabilityUnavailable')))
1086
+ const balanceText = renderBalance(balance, t)
1087
+ if (balanceText !== null) {
1088
+ tags.push(h('span', { key: 'sep4', className: 'dsh-llm-hub-meta-sep' }, '·'))
1089
+ tags.push(h('span', { key: 'bal', className: 'dsh-llm-hub-meta-text' }, balanceText))
1090
+ if (warning !== null && warning.level === 'low') {
1091
+ tags.push(h('span', { key: 'warn', className: 'dsh-llm-hub-warn-badge', title: warning.text }, '⚠️ 额度偏低'))
1092
+ }
835
1093
  }
1094
+
1095
+ // 收拢的操作按钮(Ghost 风格)
836
1096
  const noBaseURL = status.baseURL === undefined || status.baseURL === ''
837
- const probeButton = (key, label, busy, onClick) => h('button', {
1097
+ const probeBtn = (key, label, busy, spinning, onClick) => h('button', {
838
1098
  key,
839
1099
  type: 'button',
840
- className: 'dsh-llm-hub-actions__button',
1100
+ className: 'dsh-llm-hub-btn',
841
1101
  disabled: busy,
842
1102
  onClick
843
- }, label)
844
- if (noBaseURL) {
845
- actions.push(h('span', { key: 'nobase', className: 'dsh-llm-hub-balance__breakdown' }, t('noBaseURLHint')))
846
- } else {
847
- const probing = probe !== null && probe.phase === 'loading'
848
- actions.push(probeButton('probe', probing ? t('probing') : t('probe'), probing, runProbe))
849
- if (probe !== null && probe.phase === 'done') {
850
- const body = probe.body
851
- if (body?.ok === true && body.reachable === true) {
852
- actions.push(h('span', { key: 'probe-ok', className: 'dsh-llm-hub-balance__breakdown' }, `${t('reachable')} · ${body.latencyMs}ms · ${t('remoteCount')} ${body.remoteCount}`))
853
- } else {
854
- actions.push(h('span', { key: 'probe-bad', className: 'dsh-llm-hub-balance__error' }, `${t('unreachable')}: ${body?.error ?? ''}`))
855
- }
856
- }
857
- // 目录拉取原先只对 modelgo 开放(它走 anthropic-messages,官方发现天然失效)。
858
- // 但「网关上到底有哪些模型」对每个 provider 都有用 —— 智谱手填 8 个、网关在售
859
- // 16 个,不拉一次根本不知道漏了什么。改为所有 pi-ai provider 都能拉。
860
- // 探测不到的(没 baseURL 又不在已知网关表里)点了会得到明确提示,不会静默。
1103
+ }, spinning ? h(RefreshIcon, { spinning: true }) : null, label)
1104
+
1105
+ const btnGroup = [
1106
+ h(ApiKeyBadge, {
1107
+ key: 'key',
1108
+ apiKey: status.apiKey,
1109
+ keyMasked: status.keyMasked,
1110
+ t
1111
+ })
1112
+ ]
1113
+
1114
+ if (!noBaseURL) {
861
1115
  const pulling = catalog !== null && catalog.phase === 'loading'
862
- actions.push(probeButton('catalog', pulling ? t('pulling') : t('pullCatalog'), pulling, pullCatalog))
1116
+ btnGroup.push(probeBtn('probe', probing ? t('probing') : t('probe'), probing, probing, runProbe))
1117
+ btnGroup.push(probeBtn('catalog', pulling ? t('pulling') : t('pullCatalog'), pulling, pulling, pullCatalog))
863
1118
  }
864
- // 配额不可用时的上游原因不进正文,只留在 title 里(见 renderBalance 的注释)。
1119
+
1120
+ // 配额不可用时的上游原因不进正文,只留在 title 里
865
1121
  const quotaNote = balance !== null && typeof balance === 'object' && balance.ok === true
866
1122
  && balance.available === false && typeof balance.reason === 'string' && balance.reason.length > 0
867
1123
  ? balance.reason
@@ -871,12 +1127,8 @@ window.__ModuleLoader__.load({
871
1127
  if (!noBaseURL && catalog !== null && catalog.phase === 'done') {
872
1128
  const body = catalog.body
873
1129
  if (body?.ok === true && Array.isArray(body.models)) {
874
- actions.push(h('span', { key: 'cat-count', className: 'dsh-llm-hub-balance__breakdown' }, `${t('remoteCount')} ${body.models.length} · ${body.latencyMs}ms`))
875
- actions.push(probeButton('copy', copied ? t('copied') : t('copyIds'), false, copyIds))
1130
+ btnGroup.push(probeBtn('copy', copied ? t('copied') : t('copyIds'), false, false, copyIds))
876
1131
 
877
- // 拉到目录就把它摆出来。之前这里只报一个数字加「复制全部 id」,
878
- // 等于让人把 71 个 id 粘到配置文件里自己挑 —— 最后一公里留给了人。
879
- // 已配置的默认勾上,人只需要动增量。
880
1132
  const configured = new Set(Array.isArray(status.modelIds) ? status.modelIds : [])
881
1133
  const picked = selected === null ? configured : selected
882
1134
  const toggle = (id) => {
@@ -889,14 +1141,13 @@ window.__ModuleLoader__.load({
889
1141
  picker = h('div', { className: 'dsh-llm-hub-picker' },
890
1142
  h('div', { className: 'dsh-llm-hub-picker__head' },
891
1143
  h('span', null, `${t('pick')}(${picked.size}/${body.models.length})`),
892
- probeButton('save', saveState === 'saving' ? t('saving') : saveState === 'done' ? t('saved') : saveState === 'fail' ? t('saveFail') : t('save'), saveState === 'saving', saveModels)),
1144
+ probeBtn('save', saveState === 'saving' ? t('saving') : saveState === 'done' ? t('saved') : saveState === 'fail' ? t('saveFail') : t('save'), saveState === 'saving', saveState === 'saving', saveModels)),
893
1145
  h('div', { className: 'dsh-llm-hub-picker__list' },
894
1146
  body.models.map((model) => {
895
1147
  const alias = aliasesStore.aliasFor(provider, model.id)
896
1148
  return h('label', {
897
1149
  key: model.id,
898
1150
  className: 'dsh-llm-hub-picker__item',
899
- // 完整 id 永远在 title 里 —— 复制 model id 粘配置用。
900
1151
  title: model.id
901
1152
  },
902
1153
  h('input', { type: 'checkbox', checked: picked.has(model.id), onChange: () => toggle(model.id) }),
@@ -904,25 +1155,32 @@ window.__ModuleLoader__.load({
904
1155
  configured.has(model.id) ? h('span', { className: 'dsh-llm-hub-picker__tag' }, t('configured')) : null)
905
1156
  })))
906
1157
  } else {
907
- actions.push(h('span', { key: 'cat-bad', className: 'dsh-llm-hub-balance__error' }, `${t('failed')}: ${body?.error ?? ''}`))
1158
+ btnGroup.push(h('span', { key: 'cat-bad', className: 'dsh-llm-hub-error' }, `${t('failed')}: ${body?.error ?? ''}`))
908
1159
  }
909
1160
  }
1161
+
1162
+ const actionsRight = []
1163
+ if (hiddenFromDropdown) {
1164
+ actionsRight.push(h('span', {
1165
+ key: 'hidden-chip',
1166
+ className: 'dsh-llm-hub-chip dsh-llm-hub-chip--hidden',
1167
+ title: typeof verdict?.reason === 'string' ? verdict.reason : ''
1168
+ }, t('availabilityUnavailable')))
1169
+ }
1170
+
910
1171
  return h('div', null,
911
- h('div', { className: 'dsh-llm-hub-balance dsh-llm-hub-balance--stack', title: quotaNote },
912
- h('div', { key: 'facts', className: 'dsh-llm-hub-balance__facts' }, facts),
913
- h('div', { key: 'actions', className: 'dsh-llm-hub-actions' }, actions)),
1172
+ h('div', { className: 'dsh-llm-hub-card', title: quotaNote },
1173
+ h('div', { key: 'meta', className: 'dsh-llm-hub-card__meta' },
1174
+ h('div', { className: 'dsh-llm-hub-card__tags' }, tags),
1175
+ h('div', { className: 'dsh-llm-hub-card__btn-group dsh-llm-hub-actions' },
1176
+ btnGroup,
1177
+ actionsRight.length > 0 ? actionsRight : null
1178
+ )
1179
+ )
1180
+ ),
914
1181
  picker)
915
1182
  }
916
1183
 
917
- /**
918
- * 模型页页脚:插件版本 + 仓库 + 反馈入口。
919
- *
920
- * 2026-09-15 owner:「人家写的插件都有,你也加上,不然别人没有反馈,没法闭环」。
921
- * 放在 settings.models.footer 而不是每张 provider 卡片上 —— 卡片有几个就会重复
922
- * 几次,而这行信息整页只需要一份。
923
- * @param props - 注入的 `t`。
924
- * @returns 页脚节点;元信息读不到时返回 null(不占位、不报错)。
925
- */
926
1184
  /**
927
1185
  * 外部 harness 一览。
928
1186
  *
@@ -974,101 +1232,153 @@ window.__ModuleLoader__.load({
974
1232
  }
975
1233
 
976
1234
  /**
977
- * 健康看板卡:把所有 provider 的探测结果铺成一张表,每行:
978
- * 名称 | 状态(可用/不可用/未知) | 延迟 | HTTP | 原因 | 探测时间
979
- *
980
- * 不复用 availability payload:那个为下拉隐藏服务,shape 不一样;新加
981
- * `/api/dsh-llm-hub/health` 路由返回带 latencyMs / status 的全字段。
982
- * 状态 chip 与隐藏下拉那枚 chip 共用样式变量。
983
- *
984
- * 「重新探测」按钮 = availability 路由的 recheck(共用同一条底层 refresh
985
- * 流程,避免双探),触发后乐观更新 checkedAt 让用户看见反馈。
986
- *
987
- * @param props - 注入的 `t` 和 `availability`(recheck 用)。
988
- * @returns 健康看板;探测数据为空时返回 null(启动竞态)。
1235
+ * 健康看板卡:健康度已直接融合进 Provider 列表,下方不再冗余平铺整张大表。
1236
+ * 保持组件注册以保证向前兼容。
989
1237
  */
990
1238
  function HealthCard(props) {
1239
+ return null
1240
+ }
1241
+
1242
+ /**
1243
+ * 热门大模型预设库定义
1244
+ */
1245
+ const PRESETS = [
1246
+ {
1247
+ id: 'deepseek',
1248
+ name: '🇨🇳 DeepSeek 官方',
1249
+ desc: 'V4.1 Flash / V4 Pro 极速官方直连',
1250
+ docUrl: 'https://platform.deepseek.com/api_keys',
1251
+ 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'
1252
+ },
1253
+ {
1254
+ id: 'minimax',
1255
+ name: '⚡ MiniMax',
1256
+ desc: 'MiniMax-M3 百万上下文超强大模型',
1257
+ docUrl: 'https://platform.minimaxi.com/user-center/basic-information/interface-key',
1258
+ 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'
1259
+ },
1260
+ {
1261
+ id: 'zhipu',
1262
+ name: '🧠 智谱清言 GLM',
1263
+ desc: 'GLM-5 / GLM-4.7 国内 Coding 利器',
1264
+ docUrl: 'https://open.bigmodel.cn/usercenter/apikeys',
1265
+ 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'
1266
+ },
1267
+ {
1268
+ id: 'volc',
1269
+ name: '🌋 火山方舟 Ark',
1270
+ desc: '字节豆包 Doubao-1.5 多模态矩阵',
1271
+ docUrl: 'https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey',
1272
+ 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'
1273
+ },
1274
+ {
1275
+ id: 'siliconflow',
1276
+ name: '🚀 硅基流动',
1277
+ desc: 'SiliconFlow 聚合开源高性价比模型',
1278
+ docUrl: 'https://cloud.siliconflow.cn/account/ak',
1279
+ 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'
1280
+ },
1281
+ {
1282
+ id: 'openrouter',
1283
+ name: '🌐 OpenRouter',
1284
+ desc: '聚合全球 Claude / GPT / Gemini 全模型',
1285
+ docUrl: 'https://openrouter.ai/keys',
1286
+ 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'
1287
+ }
1288
+ ]
1289
+
1290
+ /**
1291
+ * 顶部 Hero 统计横条:4 核心极客 KPI
1292
+ */
1293
+ function HeroOverviewCard(props) {
991
1294
  const t = typeof props.t === 'function' ? props.t : fallbackT
992
1295
  const availability = props.availability
993
- const [report, setReport] = React.useState(null)
994
- const [busy, setBusy] = React.useState(false)
995
- const load = React.useCallback(() => {
996
- fetch(HEALTH_URL, { headers: { accept: 'application/json' } })
997
- .then((res) => res.json())
998
- .then((body) => { setReport(body !== null && typeof body === 'object' ? body : { ok: false }) })
999
- .catch(() => setReport({ ok: false }))
1000
- }, [])
1001
- React.useEffect(() => { load() }, [load])
1002
- // availability store 推 probing=true 时也刷一次 —— 探测完成后数据更全
1003
1296
  const availabilityState = availability === undefined
1004
- ? { probing: false }
1297
+ ? { providers: [] }
1005
1298
  : React.useSyncExternalStore(availability.subscribe, availability.snapshot)
1006
- React.useEffect(() => {
1007
- if (availabilityState.probing === false) load()
1008
- }, [availabilityState.probing, load])
1009
- if (report === null) return null
1010
- if (report.ok !== true) return null
1011
- const providers = Array.isArray(report.providers) ? report.providers : []
1012
- if (providers.length === 0) {
1013
- ensureStyle()
1014
- return h('div', { className: 'dsh-llm-hub-health' },
1015
- h('div', { className: 'dsh-llm-hub-health__head' },
1016
- h('span', { className: 'dsh-llm-hub-health__title' }, t('healthTitle')),
1017
- h('span', null, t('healthNoData'))))
1018
- }
1299
+
1300
+ const list = Array.isArray(availabilityState.providers) ? availabilityState.providers : []
1301
+ const totalProviders = list.length
1302
+ const readyProviders = list.filter((p) => p.state !== 'unavailable').length
1303
+ const totalModels = list.reduce((acc, p) => acc + (typeof p.modelCount === 'number' ? p.modelCount : 0), 0)
1304
+
1305
+ const latencies = list.map((p) => p.latencyMs).filter((l) => typeof l === 'number' && l > 0)
1306
+ const minLatency = latencies.length > 0 ? Math.min(...latencies) : null
1307
+
1019
1308
  ensureStyle()
1020
- const recheck = () => {
1021
- if (availability === undefined) return
1022
- setBusy(true)
1023
- availability.recheck().finally(() => { setBusy(false); load() })
1024
- }
1025
- const formatLatency = (value) => typeof value === 'number' ? `${value}ms` : '—'
1026
- const formatCheckedAt = (ms) => {
1027
- if (typeof ms !== 'number' || ms <= 0) return '—'
1028
- const d = new Date(ms)
1029
- const hh = String(d.getHours()).padStart(2, '0')
1030
- const mm = String(d.getMinutes()).padStart(2, '0')
1031
- const ss = String(d.getSeconds()).padStart(2, '0')
1032
- return `${hh}:${mm}:${ss}`
1033
- }
1034
- const stateLabel = (state) => {
1035
- const map = typeof t('healthState') === 'object' ? t('healthState') : { available: 'up', unavailable: 'down', unknown: 'unknown' }
1036
- return map[state] ?? state
1309
+
1310
+ return h('div', { className: 'dsh-llm-hub-hero' },
1311
+ h('div', { className: 'dsh-llm-hub-hero__card' },
1312
+ h('span', { className: 'dsh-llm-hub-hero__num' }, totalProviders > 0 ? `${readyProviders} / ${totalProviders}` : '—'),
1313
+ h('span', { className: 'dsh-llm-hub-hero__label' }, t('heroProviders'))),
1314
+ h('div', { className: 'dsh-llm-hub-hero__card' },
1315
+ h('span', { className: 'dsh-llm-hub-hero__num' }, totalModels > 0 ? `${totalModels}` : '—'),
1316
+ h('span', { className: 'dsh-llm-hub-hero__label' }, t('heroModels'))),
1317
+ h('div', { className: 'dsh-llm-hub-hero__card' },
1318
+ h('span', { className: 'dsh-llm-hub-hero__num' }, minLatency !== null ? `${minLatency}ms` : '—'),
1319
+ h('span', { className: 'dsh-llm-hub-hero__label' }, t('heroFastest'))),
1320
+ h('div', { className: 'dsh-llm-hub-hero__card' },
1321
+ h('span', { className: 'dsh-llm-hub-hero__num' }, '🛡️ 100%'),
1322
+ h('span', { className: 'dsh-llm-hub-hero__label' }, t('heroSecSafe'))))
1323
+ }
1324
+
1325
+ /**
1326
+ * 热门大模型一键装配抽屉
1327
+ */
1328
+ function QuickPresetsCard(props) {
1329
+ const t = typeof props.t === 'function' ? props.t : fallbackT
1330
+ const [expanded, setExpanded] = React.useState(false)
1331
+ const [selected, setSelected] = React.useState(null)
1332
+ const [copied, setCopied] = React.useState(false)
1333
+
1334
+ ensureStyle()
1335
+
1336
+ const copyYaml = (e, yaml) => {
1337
+ e.stopPropagation()
1338
+ navigator.clipboard?.writeText(yaml).then(() => {
1339
+ setCopied(true)
1340
+ setTimeout(() => setCopied(false), 2000)
1341
+ }).catch(() => {})
1037
1342
  }
1038
- const stateClass = (state) => `dsh-llm-hub-health__chip dsh-llm-hub-health__chip--${state ?? 'unknown'}`
1039
- const rows = providers.map((entry) => h('div', { key: entry.provider, className: 'dsh-llm-hub-health__row' },
1040
- h('span', { className: 'dsh-llm-hub-health__name' }, entry.displayName ?? entry.provider),
1041
- h('span', { className: stateClass(entry.state), title: typeof entry.code === 'string' ? entry.code : '' }, stateLabel(entry.state)),
1042
- h('span', { className: 'dsh-llm-hub-health__metric' }, `${t('healthLatency')} ${formatLatency(entry.latencyMs)}`),
1043
- h('span', { className: 'dsh-llm-hub-health__metric' }, `${t('healthStatus')} ${typeof entry.status === 'number' ? entry.status : '—'}`),
1044
- typeof entry.reason === 'string' && entry.reason.length > 0
1045
- ? h('span', { className: 'dsh-llm-hub-health__reason', title: entry.reason }, entry.reason)
1046
- : null,
1047
- h('span', { className: 'dsh-llm-hub-health__metric dsh-llm-hub-health__metric--time' }, `${t('healthCheckedAt')} ${formatCheckedAt(entry.checkedAt)}`)))
1048
- return h('div', { className: 'dsh-llm-hub-health' },
1049
- h('div', { className: 'dsh-llm-hub-health__head' },
1050
- h('span', { className: 'dsh-llm-hub-health__title' }, t('healthTitle')),
1051
- availability === undefined ? null : h('button', {
1052
- key: 'recheck',
1053
- type: 'button',
1054
- className: 'dsh-llm-hub-foot__link',
1055
- disabled: busy,
1056
- onClick: recheck
1057
- }, busy ? t('healthRechecking') : t('healthRecheck'))),
1058
- h('div', { className: 'dsh-llm-hub-health__rows' }, rows))
1343
+
1344
+ return h('div', { className: 'dsh-llm-hub-presets' },
1345
+ h('div', {
1346
+ className: 'dsh-llm-hub-presets__head',
1347
+ onClick: () => setExpanded((prev) => !prev)
1348
+ },
1349
+ h('span', { className: 'dsh-llm-hub-presets__title' },
1350
+ t('presetsTitle'),
1351
+ h('span', { style: { fontSize: '10px', opacity: 0.6 } }, expanded ? ' ▲ 收起' : ' ▼ 展开模版')),
1352
+ h('span', { className: 'dsh-llm-hub-subtext' }, t('presetsSub'))),
1353
+ expanded ? h('div', { className: 'dsh-llm-hub-presets__grid' },
1354
+ PRESETS.map((p) => h('div', {
1355
+ key: p.id,
1356
+ className: 'dsh-llm-hub-presets__item',
1357
+ onClick: () => setSelected(selected?.id === p.id ? null : p)
1358
+ },
1359
+ h('span', { className: 'dsh-llm-hub-presets__name' }, p.name),
1360
+ h('span', { className: 'dsh-llm-hub-presets__desc' }, p.desc)))) : null,
1361
+ expanded && selected !== null ? h('div', { className: 'dsh-llm-hub-presets__detail' },
1362
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
1363
+ h('span', { style: { fontWeight: '600' } }, `${selected.name} 配置模版`),
1364
+ h('div', { style: { display: 'flex', gap: '6px' } },
1365
+ h('button', {
1366
+ type: 'button',
1367
+ className: 'dsh-llm-hub-btn',
1368
+ onClick: (e) => copyYaml(e, selected.yaml)
1369
+ }, copied ? t('presetsCopied') : t('presetsCopyBtn')),
1370
+ h('a', {
1371
+ href: selected.docUrl,
1372
+ target: '_blank',
1373
+ rel: 'noreferrer',
1374
+ className: 'dsh-llm-hub-btn',
1375
+ style: { textDecoration: 'none' }
1376
+ }, t('presetsDocsBtn')))),
1377
+ h('pre', { className: 'dsh-llm-hub-presets__code' }, selected.yaml)) : null)
1059
1378
  }
1060
1379
 
1061
1380
  /**
1062
- * 多账号 key 轮换卡 —— 显示每个 provider 当前轮到了哪个 key。
1063
- *
1064
- * 设计为调试 / 可见性卡:用户想知道「我现在用的是哪一把 key」「下一把是谁」
1065
- * 「上次失败用的是哪个 env」。**不**显示 apiKey 实际值,只显示 name + env 名。
1066
- *
1067
- * 暂不做「下一把」按钮 —— host 半 round-robin 已经在每次解析时推进,用户
1068
- * 想强制换的话只要刷一次页面(或重探 health)就轮到下一个。
1069
- *
1070
- * @param props - 注入的 `t`。
1071
- * @returns 轮换卡;没配 keyPool 时返 null。
1381
+ * 多账号 key 轮换卡:展示当前轮换池状态与活跃 Key
1072
1382
  */
1073
1383
  function KeyPoolCard(props) {
1074
1384
  const t = typeof props.t === 'function' ? props.t : fallbackT
@@ -1092,7 +1402,12 @@ window.__ModuleLoader__.load({
1092
1402
  }
1093
1403
  const rows = providers.map((entry) => {
1094
1404
  const chosen = entry.lastChosen
1095
- return h('div', { key: entry.provider, className: 'dsh-llm-hub-keypool__row' },
1405
+ const isActive = chosen !== null
1406
+ return h('div', {
1407
+ key: entry.provider,
1408
+ className: 'dsh-llm-hub-keypool__row' + (isActive ? ' is-active' : '')
1409
+ },
1410
+ h('span', { className: 'dsh-llm-hub-health-dot ' + (isActive ? 'is-ok' : '') }),
1096
1411
  h('span', { className: 'dsh-llm-hub-keypool__name' }, entry.provider),
1097
1412
  h('span', { className: 'dsh-llm-hub-keypool__meta' }, `${entry.total} keys`),
1098
1413
  chosen === null
@@ -1103,8 +1418,8 @@ window.__ModuleLoader__.load({
1103
1418
  })
1104
1419
  return h('div', { className: 'dsh-llm-hub-keypool' },
1105
1420
  h('div', { className: 'dsh-llm-hub-keypool__head' },
1106
- h('span', { className: 'dsh-llm-hub-keypool__title' }, t('keypoolTitle')),
1107
- h('span', null, t('keypoolRound'))),
1421
+ h('span', { className: 'dsh-llm-hub-keypool__title' }, '🔄 ' + t('keypoolTitle')),
1422
+ h('span', { className: 'dsh-llm-hub-subtext' }, t('keypoolRound'))),
1108
1423
  h('div', { className: 'dsh-llm-hub-keypool__rows' }, rows))
1109
1424
  }
1110
1425
 
@@ -1152,11 +1467,7 @@ window.__ModuleLoader__.load({
1152
1467
  if (report.ok !== true) return null
1153
1468
  const candidates = Array.isArray(report.candidates) ? report.candidates : []
1154
1469
  if (candidates.length === 0) {
1155
- ensureStyle()
1156
- return h('div', { className: 'dsh-llm-hub-routing' },
1157
- h('div', { className: 'dsh-llm-hub-routing__head' },
1158
- h('span', { className: 'dsh-llm-hub-routing__title' }, t('routingTitle')),
1159
- h('span', null, t('routingUnconfigured'))))
1470
+ return null
1160
1471
  }
1161
1472
  ensureStyle()
1162
1473
  const active = report.active
@@ -1169,21 +1480,22 @@ window.__ModuleLoader__.load({
1169
1480
  h('span', { className: 'dsh-llm-hub-routing__title' }, t('routingTitle')),
1170
1481
  active !== null
1171
1482
  ? h('span', { className: 'dsh-llm-hub-routing__active' },
1172
- `${t('routingActive')}: ${active.provider}/${active.model}`,
1173
- h('span', { className: chipClass(active.state), style: { marginLeft: '6px' } }, active.state))
1174
- : null)
1175
- const rows = candidates.map((entry, index) => h('div', { key: `${entry.provider}/${entry.model}/${index}`, className: 'dsh-llm-hub-routing__row' },
1176
- h('span', { className: 'dsh-llm-hub-routing__role' }, index === 0 ? t('routingPrimary') : `${t('routingFallback')} ${index}`),
1483
+ `${t('routingActive')}: ${activeKey}`,
1484
+ h('span', { className: chipClass(active.state) }, active.state === 'available' ? '可用' : '不可用'))
1485
+ : h('span', null, t('routingUnconfigured')))
1486
+ const rows = candidates.map((entry) => h('div', { key: `${entry.provider}/${entry.model}`, className: 'dsh-llm-hub-routing__row' },
1487
+ h('span', { className: 'dsh-llm-hub-routing__role' }, entry.role === 'primary' ? t('routingPrimary') : t('routingFallback')),
1177
1488
  h('span', { className: 'dsh-llm-hub-routing__name' }, `${entry.provider}/${entry.model}`),
1178
- h('span', { className: chipClass(entry.state) }, entry.state)))
1179
- const footer = showSuggestion
1180
- ? h('div', { className: 'dsh-llm-hub-routing__suggestion' },
1181
- h('span', null, `${t('routingSuggestion')} `),
1182
- h('strong', null, `${recommendation.provider}/${recommendation.model}`))
1183
- : (typeof report.reason === 'string' && report.reason.length > 0
1184
- ? h('div', { className: 'dsh-llm-hub-routing__reason' }, report.reason)
1489
+ h('span', { className: chipClass(entry.state) }, entry.state === 'available' ? '可用' : '不可用')))
1490
+ return h('div', { className: 'dsh-llm-hub-routing' },
1491
+ head,
1492
+ h('div', { className: 'dsh-llm-hub-routing__rows' }, rows),
1493
+ showSuggestion
1494
+ ? h('div', { className: 'dsh-llm-hub-routing__suggestion' }, `${t('routingSuggestion')}: ${recKey}`)
1495
+ : null,
1496
+ recommendation === null && candidates.length > 0
1497
+ ? h('div', { className: 'dsh-llm-hub-routing__reason' }, t('routingNoRecommendation'))
1185
1498
  : null)
1186
- return h('div', { className: 'dsh-llm-hub-routing' }, head, h('div', { className: 'dsh-llm-hub-routing__rows' }, rows), footer)
1187
1499
  }
1188
1500
 
1189
1501
  /**
@@ -1198,14 +1510,7 @@ window.__ModuleLoader__.load({
1198
1510
  }
1199
1511
 
1200
1512
  /**
1201
- * 本月用量卡。
1202
- *
1203
- * 显示 input / output / cache tokens + 调用次数 + 前 5 个最常用模型。
1204
- * 不算钱 —— open source DSH 没收 chat 文本定价接口,每个 provider 写硬编码价目表
1205
- * 既过时又快塌。把 token 数留给用户对照官方价目表,导出按钮拿整张 CSV。
1206
- *
1207
- * @param props - 注入的 `t`。
1208
- * @returns 月度用量卡;统计未启用时返回 null(harness 一览照常显示)。
1513
+ * 用量卡 —— 本月调用与 token 统计。
1209
1514
  */
1210
1515
  function UsageCard(props) {
1211
1516
  const t = typeof props.t === 'function' ? props.t : fallbackT
@@ -1222,15 +1527,11 @@ window.__ModuleLoader__.load({
1222
1527
  }, [])
1223
1528
  if (report === null) return null
1224
1529
  if (report.ready !== true) {
1225
- // 用量统计未启用:照实说,不占位。
1226
- ensureStyle()
1227
- return h('div', { className: 'dsh-llm-hub-usage' },
1228
- h('div', { className: 'dsh-llm-hub-usage__head' },
1229
- h('span', { className: 'dsh-llm-hub-usage__title' }, t('usageTitle')),
1230
- h('span', null, t('usageDisabled'))))
1530
+ return null
1231
1531
  }
1232
1532
  ensureStyle()
1233
1533
  const current = report.current
1534
+ const hasCalls = (current?.calls ?? 0) > 0
1234
1535
  const topModels = Array.isArray(current?.topModels) ? current.topModels : []
1235
1536
  const exportCsv = () => {
1236
1537
  setBusy(true)
@@ -1258,8 +1559,8 @@ window.__ModuleLoader__.load({
1258
1559
  .finally(() => setBusy(false))
1259
1560
  }
1260
1561
  const facts = []
1261
- if (current) {
1262
- facts.push(h('span', { key: 'calls', className: 'dsh-llm-hub-usage__tag' }, `${current.calls ?? 0} ${t('usageCalls')}`))
1562
+ if (hasCalls) {
1563
+ facts.push(h('span', { key: 'calls', className: 'dsh-llm-hub-usage__tag' }, `${current.calls} ${t('usageCalls')}`))
1263
1564
  facts.push(h('span', { key: 'in', className: 'dsh-llm-hub-usage__tag' }, `${t('usageInput')} ${formatTokens(current.inputTokens)}`))
1264
1565
  facts.push(h('span', { key: 'out', className: 'dsh-llm-hub-usage__tag' }, `${t('usageOutput')} ${formatTokens(current.outputTokens)}`))
1265
1566
  if ((current.cacheReadTokens ?? 0) > 0 || (current.cacheWriteTokens ?? 0) > 0) {
@@ -1267,31 +1568,34 @@ window.__ModuleLoader__.load({
1267
1568
  }
1268
1569
  facts.push(h('span', { key: 'total', className: 'dsh-llm-hub-usage__tag' }, `${t('usageTotal')} ${formatTokens(current.totalTokens)}`))
1269
1570
  }
1270
- const topSection = topModels.length > 0
1271
- ? h('div', { className: 'dsh-llm-hub-usage__top' },
1272
- h('div', { className: 'dsh-llm-hub-usage__title' }, t('usageTopModels')),
1273
- topModels.map((row, index) => h('div', { key: `${row.provider}/${row.model}/${index}`, className: 'dsh-llm-hub-usage__top-row' },
1274
- h('span', null, `${row.provider}/${row.model}`),
1275
- h('small', null, `${row.calls} ${t('usageCalls')}`))))
1276
- : h('div', null, t('usageNoData'))
1277
1571
  return h('div', { className: 'dsh-llm-hub-usage' },
1278
1572
  h('div', { className: 'dsh-llm-hub-usage__head' },
1279
1573
  h('span', { className: 'dsh-llm-hub-usage__title' }, t('usageTitle')),
1280
- h('div', { className: 'dsh-llm-hub-usage__facts' }, facts)),
1281
- topSection,
1282
- h('div', { className: 'dsh-llm-hub-usage__actions' },
1283
- h('button', {
1284
- type: 'button',
1285
- className: 'dsh-llm-hub-foot__link',
1286
- disabled: busy,
1287
- onClick: exportCsv
1288
- }, feedback === 'ok' ? t('usageExported') : feedback === 'fail' ? t('usageExportFail') : t('usageExport')),
1289
- h('button', {
1290
- type: 'button',
1291
- className: 'dsh-llm-hub-foot__link',
1292
- disabled: busy || (current?.calls ?? 0) === 0,
1293
- onClick: clearRecords
1294
- }, t('usageClear'))))
1574
+ hasCalls
1575
+ ? h('div', { className: 'dsh-llm-hub-usage__facts' }, facts)
1576
+ : h('span', { className: 'dsh-llm-hub-subtext' }, t('usageNoData'))),
1577
+ hasCalls && topModels.length > 0
1578
+ ? h('div', { className: 'dsh-llm-hub-usage__top' },
1579
+ h('div', { className: 'dsh-llm-hub-usage__title' }, t('usageTopModels')),
1580
+ topModels.map((row, index) => h('div', { key: `${row.provider}/${row.model}/${index}`, className: 'dsh-llm-hub-usage__top-row' },
1581
+ h('span', null, `${row.provider}/${row.model}`),
1582
+ h('small', null, `${row.calls} ${t('usageCalls')}`))))
1583
+ : null,
1584
+ hasCalls
1585
+ ? h('div', { className: 'dsh-llm-hub-usage__actions' },
1586
+ h('button', {
1587
+ type: 'button',
1588
+ className: 'dsh-llm-hub-foot__link',
1589
+ disabled: busy,
1590
+ onClick: exportCsv
1591
+ }, feedback === 'ok' ? t('usageExported') : feedback === 'fail' ? t('usageExportFail') : t('usageExport')),
1592
+ h('button', {
1593
+ type: 'button',
1594
+ className: 'dsh-llm-hub-foot__link',
1595
+ disabled: busy,
1596
+ onClick: clearRecords
1597
+ }, t('usageClear')))
1598
+ : null)
1295
1599
  }
1296
1600
 
1297
1601
  function HubFooter(props) {
@@ -1361,6 +1665,115 @@ window.__ModuleLoader__.load({
1361
1665
  links)
1362
1666
  }
1363
1667
 
1668
+ /**
1669
+ * Webkubor DSH 扩展家族互导 Dock
1670
+ */
1671
+ function WebkuborSuiteDock(props) {
1672
+ const t = typeof props.t === 'function' ? props.t : fallbackT
1673
+ const [copiedPkg, setCopiedPkg] = React.useState(null)
1674
+
1675
+ ensureStyle()
1676
+
1677
+ const suite = [
1678
+ {
1679
+ id: '@dsh-plugins/dsh-bloom-theme',
1680
+ aliases: ['dsh-bloom-theme', '@dsh-plugins/dsh-bloom-theme'],
1681
+ name: 'Bloom Theme',
1682
+ icon: '🌸',
1683
+ desc: '落霞 Bloom 极客质感毛玻璃主题与代码高亮',
1684
+ repo: 'https://github.com/webkubor/dsh-bloom-theme',
1685
+ pkg: '@dsh-plugins/dsh-bloom-theme'
1686
+ },
1687
+ {
1688
+ id: '@dsh-plugins/dsh-llm-hub',
1689
+ aliases: ['dsh-llm-hub', '@dsh-plugins/dsh-llm-hub'],
1690
+ name: 'LLM Hub',
1691
+ icon: '🎛️',
1692
+ desc: '多模型网关、余额配额、多Key轮换与极客大屏',
1693
+ repo: 'https://github.com/webkubor/dsh-llm-hub',
1694
+ pkg: '@dsh-plugins/dsh-llm-hub',
1695
+ isCurrent: true
1696
+ },
1697
+ {
1698
+ id: '@dsh-plugins/dsh-user-mirror',
1699
+ aliases: ['dsh-user-mirror', 'dsh-mirror', '@dsh-plugins/dsh-user-mirror'],
1700
+ name: 'User Mirror',
1701
+ icon: '🪞',
1702
+ desc: '用户角色数字画像、习惯偏好与记忆网络',
1703
+ repo: 'https://github.com/webkubor/dsh-mirror',
1704
+ pkg: '@dsh-plugins/dsh-user-mirror'
1705
+ },
1706
+ {
1707
+ id: '@dsh-plugins/dsh-env-inspector',
1708
+ aliases: ['dsh-env-inspector', '@dsh-plugins/dsh-env-inspector'],
1709
+ name: 'Env Inspector',
1710
+ icon: '🖥️',
1711
+ desc: '端口监听释放、CLI工具链与环境大屏',
1712
+ repo: 'https://github.com/webkubor/dsh-env-inspector',
1713
+ pkg: '@dsh-plugins/dsh-env-inspector'
1714
+ }
1715
+ ]
1716
+
1717
+ const handleCopy = (pkg) => {
1718
+ const cmd = `dsh plugin install ${pkg}`
1719
+ if (navigator.clipboard) {
1720
+ navigator.clipboard.writeText(cmd)
1721
+ }
1722
+ setCopiedPkg(pkg)
1723
+ setTimeout(() => setCopiedPkg(null), 2000)
1724
+ }
1725
+
1726
+ return h('div', { className: 'dsh-suite-dock' },
1727
+ h('div', { className: 'dsh-suite-head' },
1728
+ h('h3', { className: 'dsh-suite-title' },
1729
+ h('span', null, '🌟'),
1730
+ t('suiteTitle')
1731
+ ),
1732
+ h('span', { className: 'dsh-suite-desc' }, t('suiteDesc'))
1733
+ ),
1734
+ h('div', { className: 'dsh-suite-cards' },
1735
+ ...suite.map((item) => {
1736
+ const isInstalled = item.isCurrent
1737
+ const isCopied = copiedPkg === item.pkg
1738
+
1739
+ return h('div', { key: item.id, className: 'dsh-suite-card' },
1740
+ h('div', { className: 'dsh-suite-card-top' },
1741
+ h('span', { className: 'dsh-suite-card-icon' }, item.icon),
1742
+ h('a', {
1743
+ href: item.repo,
1744
+ target: '_blank',
1745
+ rel: 'noopener noreferrer',
1746
+ className: 'dsh-suite-card-name'
1747
+ }, item.name)
1748
+ ),
1749
+ h('div', { className: 'dsh-suite-card-desc' }, item.desc),
1750
+ h('div', { className: 'dsh-suite-card-bottom' },
1751
+ isInstalled ? (
1752
+ h('span', { className: 'dsh-suite-badge-active' },
1753
+ h('span', { className: 'dsh-suite-badge-active-dot' }),
1754
+ t('suiteActive')
1755
+ )
1756
+ ) : (
1757
+ h('button', {
1758
+ type: 'button',
1759
+ className: 'dsh-suite-btn-action',
1760
+ onClick: () => handleCopy(item.pkg),
1761
+ title: `安装命令: dsh plugin install ${item.pkg}`
1762
+ }, isCopied ? `✓ ${t('suiteCopied')}` : `⚡ ${t('suiteCopyInstall')}`)
1763
+ ),
1764
+ h('a', {
1765
+ href: item.repo,
1766
+ target: '_blank',
1767
+ rel: 'noopener noreferrer',
1768
+ className: 'dsh-suite-link'
1769
+ }, 'GitHub ↗')
1770
+ )
1771
+ )
1772
+ })
1773
+ )
1774
+ )
1775
+ }
1776
+
1364
1777
  /**
1365
1778
  * 可用性判定缓存 —— 所有会话共用一份(同一个 host、同一份设置)。
1366
1779
  *
@@ -1517,6 +1930,20 @@ window.__ModuleLoader__.load({
1517
1930
  // 第一版漏了 id,注册被静默丢弃:接口通、组件在、页面上什么都没有。
1518
1931
  // harness 一览排在 footer 那行之上(order 更小):它讲的是「能派给谁」,
1519
1932
  // 属于能力信息;footer 那行是插件自身的版本与链接,属于元信息。
1933
+ ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
1934
+ name: 'settings.models.footer',
1935
+ id: 'dsh-llm-hub-hero',
1936
+ order: 60,
1937
+ locale: NS,
1938
+ inject: () => ({ t, availability })
1939
+ }, HeroOverviewCard))
1940
+ ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
1941
+ name: 'settings.models.footer',
1942
+ id: 'dsh-llm-hub-presets',
1943
+ order: 65,
1944
+ locale: NS,
1945
+ inject: () => ({ t })
1946
+ }, QuickPresetsCard))
1520
1947
  ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
1521
1948
  name: 'settings.models.footer',
1522
1949
  id: 'dsh-llm-hub-health',
@@ -1559,6 +1986,13 @@ window.__ModuleLoader__.load({
1559
1986
  locale: NS,
1560
1987
  inject: () => ({ t, availability })
1561
1988
  }, HubFooter))
1989
+ ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
1990
+ name: 'settings.models.footer',
1991
+ id: 'dsh-llm-hub-suite',
1992
+ order: 110,
1993
+ locale: NS,
1994
+ inject: () => ({ t })
1995
+ }, WebkuborSuiteDock))
1562
1996
  }
1563
1997
 
1564
1998
  exports.apply = apply