@dsh-plugins/dsh-llm-hub 1.3.0 → 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/CHANGELOG.md +13 -0
- package/README.md +11 -4
- package/lib/client.js +685 -400
- package/lib/index.js +18 -27
- package/package.json +1 -1
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: '输入',
|
|
@@ -196,6 +215,12 @@ window.__ModuleLoader__.load({
|
|
|
196
215
|
configuredModels: 'configured',
|
|
197
216
|
modelsUnit: 'models',
|
|
198
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',
|
|
199
224
|
noBaseURLHint: 'No service address configured — probing unavailable, models are hand-typed',
|
|
200
225
|
pullCatalog: 'Fetch catalog',
|
|
201
226
|
pick: 'Pick models',
|
|
@@ -245,6 +270,19 @@ window.__ModuleLoader__.load({
|
|
|
245
270
|
aliasesMissing: 'no aliases configured',
|
|
246
271
|
keypoolTitle: 'Key rotation',
|
|
247
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 ↗',
|
|
248
286
|
usageTitle: 'This month',
|
|
249
287
|
usageCalls: 'calls',
|
|
250
288
|
usageInput: 'in',
|
|
@@ -315,150 +353,293 @@ window.__ModuleLoader__.load({
|
|
|
315
353
|
const style = document.createElement('style')
|
|
316
354
|
style.id = id
|
|
317
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 容器类名
|
|
318
369
|
'.dsh-llm-hub-balance{display:flex;align-items:center;gap:8px;flex-wrap:wrap;',
|
|
319
|
-
'margin-top:
|
|
320
|
-
'border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);',
|
|
370
|
+
'margin-top:4px;padding:4px 0;border:none;background:transparent;',
|
|
321
371
|
'font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
|
|
322
|
-
'.dsh-llm-hub-
|
|
323
|
-
'
|
|
324
|
-
'.dsh-llm-hub-
|
|
325
|
-
'
|
|
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);',
|
|
326
405
|
'background:var(--dsw-alias-bg-layer-2)}',
|
|
327
|
-
'.dsh-llm-hub-
|
|
328
|
-
'
|
|
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%;',
|
|
329
441
|
'background:var(--dsw-alias-label-secondary);flex:none}',
|
|
330
|
-
'.dsh-llm-hub-harness__dot--ready{background:var(--dsw-alias-state-success-primary,#
|
|
442
|
+
'.dsh-llm-hub-harness__dot--ready{background:var(--dsw-alias-state-success-primary,#10b981)}',
|
|
331
443
|
'.dsh-llm-hub-harness__name{color:var(--dsw-alias-label-primary)}',
|
|
332
444
|
'.dsh-llm-hub-harness__state{opacity:.75}',
|
|
333
|
-
|
|
334
|
-
'.dsh-llm-hub-
|
|
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;',
|
|
445
|
+
// 目录选择器 Drawer
|
|
446
|
+
'.dsh-llm-hub-picker{margin-top:6px;padding:8px 10px;border-radius:6px;',
|
|
352
447
|
'border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2)}',
|
|
353
448
|
'.dsh-llm-hub-picker__head{display:flex;align-items:center;justify-content:space-between;',
|
|
354
|
-
'gap:8px;font-size:
|
|
355
|
-
'.dsh-llm-hub-picker__list{max-height:
|
|
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;',
|
|
356
451
|
'flex-direction:column;gap:2px}',
|
|
357
|
-
'.dsh-llm-hub-picker__item{display:flex;align-items:center;gap:6px;font-size:
|
|
452
|
+
'.dsh-llm-hub-picker__item{display:flex;align-items:center;gap:6px;font-size:11.5px;',
|
|
358
453
|
'line-height:20px;color:var(--dsw-alias-label-primary);cursor:pointer}',
|
|
359
454
|
'.dsh-llm-hub-picker__id{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}',
|
|
360
|
-
'.dsh-llm-hub-
|
|
361
|
-
'
|
|
362
|
-
|
|
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}',
|
|
363
461
|
'.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}',
|
|
462
|
+
'border:0;background:0 0;padding:0;font:inherit;font-size:11px;cursor:pointer}',
|
|
365
463
|
'.dsh-llm-hub-foot__link:hover{text-decoration:underline}',
|
|
366
|
-
'.dsh-llm-hub-
|
|
367
|
-
'
|
|
368
|
-
'
|
|
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)}',
|
|
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)}',
|
|
379
467
|
'.dsh-llm-hub-chip--hidden{border-color:var(--dsw-alias-state-error-primary);',
|
|
380
468
|
'color:var(--dsw-alias-state-error-primary)}',
|
|
381
469
|
'.dsh-llm-hub-foot__hidden{color:var(--dsw-alias-state-error-primary)}',
|
|
382
|
-
//
|
|
470
|
+
// 用量卡:紧凑通透,不再沉重
|
|
383
471
|
'.dsh-llm-hub-usage{display:flex;flex-direction:column;gap:6px;margin-top:10px;',
|
|
384
|
-
'padding:8px 10px;border-radius:
|
|
385
|
-
'border:.5px solid var(--dsw-alias-border-l1);background:
|
|
386
|
-
'font-size:
|
|
387
|
-
'.dsh-llm-hub-usage__head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
|
|
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}',
|
|
388
476
|
'.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:
|
|
390
|
-
'.dsh-llm-hub-usage__tag{display:inline-flex;align-items:center;gap:
|
|
391
|
-
'border-radius:
|
|
392
|
-
'background:var(--dsw-alias-bg-layer-3)}',
|
|
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}',
|
|
393
480
|
'.dsh-llm-hub-usage__top{display:flex;flex-direction:column;gap:2px;margin-top:2px}',
|
|
394
481
|
'.dsh-llm-hub-usage__top-row{display:flex;justify-content:space-between;gap:8px;',
|
|
395
|
-
'color:var(--dsw-alias-label-primary)}',
|
|
482
|
+
'color:var(--dsw-alias-label-primary);font-size:11px}',
|
|
396
483
|
'.dsh-llm-hub-usage__top-row small{color:var(--dsw-alias-label-secondary);opacity:.8}',
|
|
397
|
-
'.dsh-llm-hub-usage__actions{display:flex;gap:
|
|
398
|
-
//
|
|
399
|
-
'.dsh-llm-hub-health{display:
|
|
400
|
-
|
|
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)
|
|
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
|
+
// 当前路由卡
|
|
424
488
|
'.dsh-llm-hub-routing{display:flex;flex-direction:column;gap:6px;margin-top:10px;',
|
|
425
|
-
'padding:8px 10px;border-radius:
|
|
426
|
-
'border:.5px solid var(--dsw-alias-border-l1);background:
|
|
427
|
-
'font-size:
|
|
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)}',
|
|
428
492
|
'.dsh-llm-hub-routing__head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
|
|
429
493
|
'.dsh-llm-hub-routing__title{color:var(--dsw-alias-label-primary);font-weight:600}',
|
|
430
494
|
'.dsh-llm-hub-routing__active{color:var(--dsw-alias-label-primary)}',
|
|
431
495
|
'.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:
|
|
434
|
-
'
|
|
435
|
-
'
|
|
436
|
-
'background:var(--dsw-alias-bg-layer-3);font-size:11px;color:var(--dsw-alias-label-secondary)}',
|
|
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)}',
|
|
437
500
|
'.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:
|
|
440
|
-
|
|
441
|
-
'padding-top:4px}',
|
|
442
|
-
// 多账号 key 轮换:放在 routing 75 之前(order 73)
|
|
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 轮换
|
|
443
504
|
'.dsh-llm-hub-keypool{display:flex;flex-direction:column;gap:6px;margin-top:10px;',
|
|
444
|
-
'padding:8px 10px;border-radius:
|
|
445
|
-
'border:.5px solid var(--dsw-alias-border-l1);background:
|
|
446
|
-
'font-size:
|
|
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:
|
|
450
|
-
'.dsh-llm-hub-keypool__row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;',
|
|
451
|
-
'
|
|
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:
|
|
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}',
|
|
454
515
|
'.dsh-llm-hub-keypool__chosen{display:flex;align-items:baseline;gap:4px;',
|
|
455
516
|
'color:var(--dsw-alias-label-primary)}',
|
|
456
|
-
'.dsh-llm-hub-keypool__chosen small{color:var(--dsw-alias-label-secondary);opacity:.75;font-size:
|
|
517
|
+
'.dsh-llm-hub-keypool__chosen small{color:var(--dsw-alias-label-secondary);opacity:.75;font-size:10.5px}',
|
|
457
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)}',
|
|
458
540
|
].join('')
|
|
459
541
|
document.head.appendChild(style)
|
|
460
542
|
}
|
|
461
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
|
+
|
|
462
643
|
/**
|
|
463
644
|
* 余额卡:挂载即查,可手动刷新。
|
|
464
645
|
* @param props - owner props(provider/configured/keyConfigured)+ 注入的 `t`。
|
|
@@ -469,6 +650,7 @@ window.__ModuleLoader__.load({
|
|
|
469
650
|
const keyConfigured = props.keyConfigured !== false
|
|
470
651
|
const [state, setState] = React.useState({ status: 'idle' })
|
|
471
652
|
const [warning, setWarning] = React.useState(null)
|
|
653
|
+
const [isRefreshing, setIsRefreshing] = React.useState(false)
|
|
472
654
|
// 官方直连也可能被判不可用(key 被拒 / 账户欠费)。同一个状态片,一致的处理。
|
|
473
655
|
const availability = props.availability
|
|
474
656
|
const availabilityState = availability === undefined
|
|
@@ -484,7 +666,8 @@ window.__ModuleLoader__.load({
|
|
|
484
666
|
setState({ status: 'nokey' })
|
|
485
667
|
return
|
|
486
668
|
}
|
|
487
|
-
setState({ status: 'loading' })
|
|
669
|
+
setState((prev) => (prev.status === 'ok' ? prev : { status: 'loading' }))
|
|
670
|
+
setIsRefreshing(true)
|
|
488
671
|
fetch(BALANCE_URL, { headers: { accept: 'application/json' } })
|
|
489
672
|
.then(async (response) => {
|
|
490
673
|
let body
|
|
@@ -496,59 +679,31 @@ window.__ModuleLoader__.load({
|
|
|
496
679
|
if (body == null || body.ok !== true) {
|
|
497
680
|
throw new Error(typeof body?.error === 'string' ? body.error : `HTTP ${response.status}`)
|
|
498
681
|
}
|
|
499
|
-
setState({
|
|
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
|
+
})
|
|
500
689
|
})
|
|
501
690
|
.catch((error) => {
|
|
502
691
|
setState({ status: 'error', error: error instanceof Error ? error.message : String(error) })
|
|
503
692
|
})
|
|
693
|
+
.finally(() => {
|
|
694
|
+
setIsRefreshing(false)
|
|
695
|
+
})
|
|
504
696
|
}, [keyConfigured])
|
|
505
697
|
|
|
506
698
|
React.useEffect(() => {
|
|
507
699
|
load()
|
|
508
700
|
}, [load])
|
|
509
701
|
|
|
510
|
-
const
|
|
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
|
-
}
|
|
702
|
+
const primary = state.status === 'ok' && Array.isArray(state.balances) ? state.balances[0] : undefined
|
|
542
703
|
|
|
543
|
-
//
|
|
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' 才上色 + 提示。
|
|
704
|
+
// 余额预警:无条件在最外层声明 Hook,遵从 Rules of Hooks!
|
|
551
705
|
React.useEffect(() => {
|
|
706
|
+
if (!primary || typeof primary.total !== 'string') return
|
|
552
707
|
fetch(WARNING_URL, {
|
|
553
708
|
method: 'POST',
|
|
554
709
|
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
@@ -566,35 +721,85 @@ window.__ModuleLoader__.load({
|
|
|
566
721
|
}
|
|
567
722
|
})
|
|
568
723
|
.catch(() => setWarning(null))
|
|
569
|
-
}, [primary
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
724
|
+
}, [primary?.total, primary?.currency])
|
|
725
|
+
|
|
726
|
+
if (state.status === 'nokey') {
|
|
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
|
+
)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (state.status === 'loading' || state.status === 'idle') {
|
|
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
|
+
)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (state.status === 'error') {
|
|
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
|
+
)
|
|
754
|
+
}
|
|
755
|
+
|
|
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
|
+
)
|
|
581
762
|
}
|
|
582
|
-
|
|
763
|
+
|
|
764
|
+
const symbol = primary.currency === 'CNY' ? '¥' : primary.currency === 'USD' ? '$' : `${primary.currency} `
|
|
583
765
|
const breakdown = []
|
|
584
766
|
if (primary.granted !== '0' && primary.granted !== '0.00') breakdown.push(`${t('granted')} ${symbol}${primary.granted}`)
|
|
585
767
|
if (primary.toppedUp !== '0' && primary.toppedUp !== '0.00') breakdown.push(`${t('toppedUp')} ${symbol}${primary.toppedUp}`)
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
+
)
|
|
598
803
|
}
|
|
599
804
|
|
|
600
805
|
/**
|
|
@@ -778,90 +983,108 @@ window.__ModuleLoader__.load({
|
|
|
778
983
|
// 2026-09-15 owner 报「为啥都显示状态读取失败」—— 页面一打开、或 DSH 刚重启
|
|
779
984
|
// 导致这次 fetch 断掉时,三行全是这句,看着像插件坏了,其实只是还没加载完。
|
|
780
985
|
if (status === null) {
|
|
781
|
-
return h('div', { className: 'dsh-llm-hub-
|
|
782
|
-
h('
|
|
783
|
-
|
|
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'))))
|
|
784
989
|
}
|
|
785
990
|
if (status.ok !== true) {
|
|
786
|
-
return h('div', { className: 'dsh-llm-hub-
|
|
787
|
-
h('
|
|
788
|
-
|
|
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'))))
|
|
789
994
|
}
|
|
790
995
|
|
|
791
|
-
//
|
|
792
|
-
const
|
|
793
|
-
|
|
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
|
-
// 而域名已经够回答「连的是不是我以为的那个网关」。
|
|
996
|
+
// 元信息标签组(不重复展示外层已有的大标题,只呈现核心事实)
|
|
997
|
+
const tags = []
|
|
998
|
+
tags.push(h('span', { key: 'count', className: 'dsh-llm-hub-tag' }, `${t('configuredModels')} ${status.modelCount} ${t('modelsUnit')}`))
|
|
801
999
|
if (typeof status.api === 'string' && status.api !== '') {
|
|
802
|
-
|
|
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))
|
|
803
1002
|
}
|
|
804
1003
|
if (typeof status.baseURL === 'string' && status.baseURL !== '') {
|
|
805
1004
|
let host = status.baseURL
|
|
806
1005
|
try { host = new URL(status.baseURL).host } catch {}
|
|
807
|
-
|
|
1006
|
+
tags.push(h('span', { key: 'sep2', className: 'dsh-llm-hub-meta-sep' }, '·'))
|
|
1007
|
+
tags.push(h('span', {
|
|
808
1008
|
key: 'base',
|
|
809
|
-
className: 'dsh-llm-hub-
|
|
1009
|
+
className: 'dsh-llm-hub-meta-text',
|
|
810
1010
|
title: `${t('endpoint')}: ${status.baseURL}`
|
|
811
1011
|
}, host))
|
|
812
1012
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
+
))
|
|
821
1051
|
}
|
|
822
1052
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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')))
|
|
1053
|
+
const balanceText = renderBalance(balance, t)
|
|
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
|
+
}
|
|
835
1060
|
}
|
|
1061
|
+
|
|
1062
|
+
// 收拢的操作按钮(Ghost 风格)
|
|
836
1063
|
const noBaseURL = status.baseURL === undefined || status.baseURL === ''
|
|
837
|
-
const
|
|
1064
|
+
const probeBtn = (key, label, busy, spinning, onClick) => h('button', {
|
|
838
1065
|
key,
|
|
839
1066
|
type: 'button',
|
|
840
|
-
className: 'dsh-llm-hub-
|
|
1067
|
+
className: 'dsh-llm-hub-btn',
|
|
841
1068
|
disabled: busy,
|
|
842
1069
|
onClick
|
|
843
|
-
}, label)
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
// 目录拉取原先只对 modelgo 开放(它走 anthropic-messages,官方发现天然失效)。
|
|
858
|
-
// 但「网关上到底有哪些模型」对每个 provider 都有用 —— 智谱手填 8 个、网关在售
|
|
859
|
-
// 16 个,不拉一次根本不知道漏了什么。改为所有 pi-ai provider 都能拉。
|
|
860
|
-
// 探测不到的(没 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) {
|
|
861
1082
|
const pulling = catalog !== null && catalog.phase === 'loading'
|
|
862
|
-
|
|
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))
|
|
863
1085
|
}
|
|
864
|
-
|
|
1086
|
+
|
|
1087
|
+
// 配额不可用时的上游原因不进正文,只留在 title 里
|
|
865
1088
|
const quotaNote = balance !== null && typeof balance === 'object' && balance.ok === true
|
|
866
1089
|
&& balance.available === false && typeof balance.reason === 'string' && balance.reason.length > 0
|
|
867
1090
|
? balance.reason
|
|
@@ -871,12 +1094,8 @@ window.__ModuleLoader__.load({
|
|
|
871
1094
|
if (!noBaseURL && catalog !== null && catalog.phase === 'done') {
|
|
872
1095
|
const body = catalog.body
|
|
873
1096
|
if (body?.ok === true && Array.isArray(body.models)) {
|
|
874
|
-
|
|
875
|
-
actions.push(probeButton('copy', copied ? t('copied') : t('copyIds'), false, copyIds))
|
|
1097
|
+
btnGroup.push(probeBtn('copy', copied ? t('copied') : t('copyIds'), false, false, copyIds))
|
|
876
1098
|
|
|
877
|
-
// 拉到目录就把它摆出来。之前这里只报一个数字加「复制全部 id」,
|
|
878
|
-
// 等于让人把 71 个 id 粘到配置文件里自己挑 —— 最后一公里留给了人。
|
|
879
|
-
// 已配置的默认勾上,人只需要动增量。
|
|
880
1099
|
const configured = new Set(Array.isArray(status.modelIds) ? status.modelIds : [])
|
|
881
1100
|
const picked = selected === null ? configured : selected
|
|
882
1101
|
const toggle = (id) => {
|
|
@@ -889,14 +1108,13 @@ window.__ModuleLoader__.load({
|
|
|
889
1108
|
picker = h('div', { className: 'dsh-llm-hub-picker' },
|
|
890
1109
|
h('div', { className: 'dsh-llm-hub-picker__head' },
|
|
891
1110
|
h('span', null, `${t('pick')}(${picked.size}/${body.models.length})`),
|
|
892
|
-
|
|
1111
|
+
probeBtn('save', saveState === 'saving' ? t('saving') : saveState === 'done' ? t('saved') : saveState === 'fail' ? t('saveFail') : t('save'), saveState === 'saving', saveState === 'saving', saveModels)),
|
|
893
1112
|
h('div', { className: 'dsh-llm-hub-picker__list' },
|
|
894
1113
|
body.models.map((model) => {
|
|
895
1114
|
const alias = aliasesStore.aliasFor(provider, model.id)
|
|
896
1115
|
return h('label', {
|
|
897
1116
|
key: model.id,
|
|
898
1117
|
className: 'dsh-llm-hub-picker__item',
|
|
899
|
-
// 完整 id 永远在 title 里 —— 复制 model id 粘配置用。
|
|
900
1118
|
title: model.id
|
|
901
1119
|
},
|
|
902
1120
|
h('input', { type: 'checkbox', checked: picked.has(model.id), onChange: () => toggle(model.id) }),
|
|
@@ -904,25 +1122,32 @@ window.__ModuleLoader__.load({
|
|
|
904
1122
|
configured.has(model.id) ? h('span', { className: 'dsh-llm-hub-picker__tag' }, t('configured')) : null)
|
|
905
1123
|
})))
|
|
906
1124
|
} else {
|
|
907
|
-
|
|
1125
|
+
btnGroup.push(h('span', { key: 'cat-bad', className: 'dsh-llm-hub-error' }, `${t('failed')}: ${body?.error ?? ''}`))
|
|
908
1126
|
}
|
|
909
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
|
+
|
|
910
1138
|
return h('div', null,
|
|
911
|
-
h('div', { className: 'dsh-llm-hub-
|
|
912
|
-
h('div', { key: '
|
|
913
|
-
|
|
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
|
+
),
|
|
914
1148
|
picker)
|
|
915
1149
|
}
|
|
916
1150
|
|
|
917
|
-
/**
|
|
918
|
-
* 模型页页脚:插件版本 + 仓库 + 反馈入口。
|
|
919
|
-
*
|
|
920
|
-
* 2026-09-15 owner:「人家写的插件都有,你也加上,不然别人没有反馈,没法闭环」。
|
|
921
|
-
* 放在 settings.models.footer 而不是每张 provider 卡片上 —— 卡片有几个就会重复
|
|
922
|
-
* 几次,而这行信息整页只需要一份。
|
|
923
|
-
* @param props - 注入的 `t`。
|
|
924
|
-
* @returns 页脚节点;元信息读不到时返回 null(不占位、不报错)。
|
|
925
|
-
*/
|
|
926
1151
|
/**
|
|
927
1152
|
* 外部 harness 一览。
|
|
928
1153
|
*
|
|
@@ -974,101 +1199,153 @@ window.__ModuleLoader__.load({
|
|
|
974
1199
|
}
|
|
975
1200
|
|
|
976
1201
|
/**
|
|
977
|
-
*
|
|
978
|
-
*
|
|
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(启动竞态)。
|
|
1202
|
+
* 健康看板卡:健康度已直接融合进 Provider 列表,下方不再冗余平铺整张大表。
|
|
1203
|
+
* 保持组件注册以保证向前兼容。
|
|
989
1204
|
*/
|
|
990
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) {
|
|
991
1261
|
const t = typeof props.t === 'function' ? props.t : fallbackT
|
|
992
1262
|
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
1263
|
const availabilityState = availability === undefined
|
|
1004
|
-
? {
|
|
1264
|
+
? { providers: [] }
|
|
1005
1265
|
: React.useSyncExternalStore(availability.subscribe, availability.snapshot)
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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
|
-
}
|
|
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
|
+
|
|
1019
1275
|
ensureStyle()
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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(() => {})
|
|
1037
1309
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
h('
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
className: 'dsh-llm-hub-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
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)
|
|
1059
1345
|
}
|
|
1060
1346
|
|
|
1061
1347
|
/**
|
|
1062
|
-
* 多账号 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。
|
|
1348
|
+
* 多账号 key 轮换卡:展示当前轮换池状态与活跃 Key
|
|
1072
1349
|
*/
|
|
1073
1350
|
function KeyPoolCard(props) {
|
|
1074
1351
|
const t = typeof props.t === 'function' ? props.t : fallbackT
|
|
@@ -1092,7 +1369,12 @@ window.__ModuleLoader__.load({
|
|
|
1092
1369
|
}
|
|
1093
1370
|
const rows = providers.map((entry) => {
|
|
1094
1371
|
const chosen = entry.lastChosen
|
|
1095
|
-
|
|
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' : '') }),
|
|
1096
1378
|
h('span', { className: 'dsh-llm-hub-keypool__name' }, entry.provider),
|
|
1097
1379
|
h('span', { className: 'dsh-llm-hub-keypool__meta' }, `${entry.total} keys`),
|
|
1098
1380
|
chosen === null
|
|
@@ -1103,8 +1385,8 @@ window.__ModuleLoader__.load({
|
|
|
1103
1385
|
})
|
|
1104
1386
|
return h('div', { className: 'dsh-llm-hub-keypool' },
|
|
1105
1387
|
h('div', { className: 'dsh-llm-hub-keypool__head' },
|
|
1106
|
-
h('span', { className: 'dsh-llm-hub-keypool__title' }, t('keypoolTitle')),
|
|
1107
|
-
h('span',
|
|
1388
|
+
h('span', { className: 'dsh-llm-hub-keypool__title' }, '🔄 ' + t('keypoolTitle')),
|
|
1389
|
+
h('span', { className: 'dsh-llm-hub-subtext' }, t('keypoolRound'))),
|
|
1108
1390
|
h('div', { className: 'dsh-llm-hub-keypool__rows' }, rows))
|
|
1109
1391
|
}
|
|
1110
1392
|
|
|
@@ -1152,11 +1434,7 @@ window.__ModuleLoader__.load({
|
|
|
1152
1434
|
if (report.ok !== true) return null
|
|
1153
1435
|
const candidates = Array.isArray(report.candidates) ? report.candidates : []
|
|
1154
1436
|
if (candidates.length === 0) {
|
|
1155
|
-
|
|
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'))))
|
|
1437
|
+
return null
|
|
1160
1438
|
}
|
|
1161
1439
|
ensureStyle()
|
|
1162
1440
|
const active = report.active
|
|
@@ -1169,21 +1447,22 @@ window.__ModuleLoader__.load({
|
|
|
1169
1447
|
h('span', { className: 'dsh-llm-hub-routing__title' }, t('routingTitle')),
|
|
1170
1448
|
active !== null
|
|
1171
1449
|
? h('span', { className: 'dsh-llm-hub-routing__active' },
|
|
1172
|
-
`${t('routingActive')}: ${
|
|
1173
|
-
h('span', { className: chipClass(active.state),
|
|
1174
|
-
: null)
|
|
1175
|
-
const rows = candidates.map((entry
|
|
1176
|
-
h('span', { className: 'dsh-llm-hub-routing__role' },
|
|
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')),
|
|
1177
1455
|
h('span', { className: 'dsh-llm-hub-routing__name' }, `${entry.provider}/${entry.model}`),
|
|
1178
|
-
h('span', { className: chipClass(entry.state) }, entry.state)))
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
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'))
|
|
1185
1465
|
: null)
|
|
1186
|
-
return h('div', { className: 'dsh-llm-hub-routing' }, head, h('div', { className: 'dsh-llm-hub-routing__rows' }, rows), footer)
|
|
1187
1466
|
}
|
|
1188
1467
|
|
|
1189
1468
|
/**
|
|
@@ -1198,14 +1477,7 @@ window.__ModuleLoader__.load({
|
|
|
1198
1477
|
}
|
|
1199
1478
|
|
|
1200
1479
|
/**
|
|
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 一览照常显示)。
|
|
1480
|
+
* 用量卡 —— 本月调用与 token 统计。
|
|
1209
1481
|
*/
|
|
1210
1482
|
function UsageCard(props) {
|
|
1211
1483
|
const t = typeof props.t === 'function' ? props.t : fallbackT
|
|
@@ -1222,15 +1494,11 @@ window.__ModuleLoader__.load({
|
|
|
1222
1494
|
}, [])
|
|
1223
1495
|
if (report === null) return null
|
|
1224
1496
|
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'))))
|
|
1497
|
+
return null
|
|
1231
1498
|
}
|
|
1232
1499
|
ensureStyle()
|
|
1233
1500
|
const current = report.current
|
|
1501
|
+
const hasCalls = (current?.calls ?? 0) > 0
|
|
1234
1502
|
const topModels = Array.isArray(current?.topModels) ? current.topModels : []
|
|
1235
1503
|
const exportCsv = () => {
|
|
1236
1504
|
setBusy(true)
|
|
@@ -1258,8 +1526,8 @@ window.__ModuleLoader__.load({
|
|
|
1258
1526
|
.finally(() => setBusy(false))
|
|
1259
1527
|
}
|
|
1260
1528
|
const facts = []
|
|
1261
|
-
if (
|
|
1262
|
-
facts.push(h('span', { key: 'calls', className: 'dsh-llm-hub-usage__tag' }, `${current.calls
|
|
1529
|
+
if (hasCalls) {
|
|
1530
|
+
facts.push(h('span', { key: 'calls', className: 'dsh-llm-hub-usage__tag' }, `${current.calls} ${t('usageCalls')}`))
|
|
1263
1531
|
facts.push(h('span', { key: 'in', className: 'dsh-llm-hub-usage__tag' }, `${t('usageInput')} ${formatTokens(current.inputTokens)}`))
|
|
1264
1532
|
facts.push(h('span', { key: 'out', className: 'dsh-llm-hub-usage__tag' }, `${t('usageOutput')} ${formatTokens(current.outputTokens)}`))
|
|
1265
1533
|
if ((current.cacheReadTokens ?? 0) > 0 || (current.cacheWriteTokens ?? 0) > 0) {
|
|
@@ -1267,31 +1535,34 @@ window.__ModuleLoader__.load({
|
|
|
1267
1535
|
}
|
|
1268
1536
|
facts.push(h('span', { key: 'total', className: 'dsh-llm-hub-usage__tag' }, `${t('usageTotal')} ${formatTokens(current.totalTokens)}`))
|
|
1269
1537
|
}
|
|
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
1538
|
return h('div', { className: 'dsh-llm-hub-usage' },
|
|
1278
1539
|
h('div', { className: 'dsh-llm-hub-usage__head' },
|
|
1279
1540
|
h('span', { className: 'dsh-llm-hub-usage__title' }, t('usageTitle')),
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
className: 'dsh-llm-hub-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
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)
|
|
1295
1566
|
}
|
|
1296
1567
|
|
|
1297
1568
|
function HubFooter(props) {
|
|
@@ -1517,6 +1788,20 @@ window.__ModuleLoader__.load({
|
|
|
1517
1788
|
// 第一版漏了 id,注册被静默丢弃:接口通、组件在、页面上什么都没有。
|
|
1518
1789
|
// harness 一览排在 footer 那行之上(order 更小):它讲的是「能派给谁」,
|
|
1519
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))
|
|
1520
1805
|
ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
|
|
1521
1806
|
name: 'settings.models.footer',
|
|
1522
1807
|
id: 'dsh-llm-hub-health',
|