@dsh-plugins/dsh-llm-hub 1.3.3 → 1.5.0
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 +193 -0
- package/README.md +36 -27
- package/lib/availability-store.js +168 -0
- package/lib/client.js +322 -80
- package/lib/index.js +567 -225
- package/lib/sorter.js +106 -0
- package/package.json +19 -9
- package/types/globals.d.ts +3 -0
- package/types/index.d.ts +62 -0
package/lib/client.js
CHANGED
|
@@ -73,10 +73,13 @@ window.__ModuleLoader__.load({
|
|
|
73
73
|
const HEALTH_URL = '/api/dsh-llm-hub/health'
|
|
74
74
|
/** 智能路由建议:当前激活 + 推荐 fallback。 */
|
|
75
75
|
const ROUTING_URL = '/api/dsh-llm-hub/routing/resolve'
|
|
76
|
+
/** 路由组状态:模式、激活组、每组候选与可用性、最近实际路由。 */
|
|
77
|
+
const ROUTING_STATE_URL = '/api/dsh-llm-hub/routing/state'
|
|
78
|
+
/** 路由组切换:POST { mode?, activeGroup? },持久化进 settings。 */
|
|
79
|
+
const ROUTING_SELECT_URL = '/api/dsh-llm-hub/routing/select'
|
|
76
80
|
/** 模型别名:把长 id 显示成短名(人话)。 */
|
|
77
81
|
const ALIASES_URL = '/api/dsh-llm-hub/aliases'
|
|
78
|
-
|
|
79
|
-
const KEYPOOL_URL = '/api/dsh-llm-hub/keypool/status'
|
|
82
|
+
const BILLING_MODE_URL = '/api/dsh-llm-hub/billing-mode'
|
|
80
83
|
|
|
81
84
|
const LOCALES = {
|
|
82
85
|
zh: {
|
|
@@ -85,6 +88,10 @@ window.__ModuleLoader__.load({
|
|
|
85
88
|
refresh: '刷新余额',
|
|
86
89
|
retry: '重试',
|
|
87
90
|
noKey: '未配置 API Key',
|
|
91
|
+
billingUsage: '按量付费',
|
|
92
|
+
billingSub: '套餐',
|
|
93
|
+
billingSwitchToSub: '切换为套餐模式',
|
|
94
|
+
billingSwitchToUsage: '切换为按量付费模式',
|
|
88
95
|
noKeyHint: '在下方填入 DeepSeek API Key 后即可查询余额。',
|
|
89
96
|
granted: '赠送',
|
|
90
97
|
toppedUp: '充值',
|
|
@@ -157,11 +164,15 @@ window.__ModuleLoader__.load({
|
|
|
157
164
|
routingPrimary: '主力',
|
|
158
165
|
routingNoRecommendation: '全部不可用',
|
|
159
166
|
routingLoading: '路由解析中…',
|
|
167
|
+
routeChipSmart: '智能',
|
|
168
|
+
routeChipManual: '手动',
|
|
169
|
+
routeChipModeSmart: '智能路由',
|
|
170
|
+
routeChipModeManual: '手动模型',
|
|
171
|
+
routeChipWouldRoute: '将路由到',
|
|
172
|
+
routeChipLastRoute: '最近实际',
|
|
173
|
+
routeChipReasonNone: '候选全部不可用',
|
|
174
|
+
routeChipFailOpen: '路由失败时按原配置发出',
|
|
160
175
|
aliasesMissing: '尚未配置别名',
|
|
161
|
-
keypoolTitle: '多账号 key 轮换',
|
|
162
|
-
keypoolRound: 'round-robin',
|
|
163
|
-
keypoolActiveTip: '当前优先使用',
|
|
164
|
-
keypoolNextTip: '下次轮换目标',
|
|
165
176
|
heroTitle: 'LLM 矩阵概览',
|
|
166
177
|
heroProviders: '活跃提供方',
|
|
167
178
|
heroModels: '可用模型',
|
|
@@ -191,6 +202,7 @@ window.__ModuleLoader__.load({
|
|
|
191
202
|
suiteTitle: 'Webkubor DSH 扩展家族',
|
|
192
203
|
suiteDesc: '一键构建完整智能体开发环境',
|
|
193
204
|
suiteActive: '已激活',
|
|
205
|
+
suiteUnknown: '判定不可用',
|
|
194
206
|
suiteCopyInstall: '复制安装',
|
|
195
207
|
suiteCopied: '已复制命令'
|
|
196
208
|
},
|
|
@@ -201,6 +213,10 @@ window.__ModuleLoader__.load({
|
|
|
201
213
|
retry: 'Retry',
|
|
202
214
|
noKey: 'No API key',
|
|
203
215
|
noKeyHint: 'Add a DeepSeek API key below to see the balance.',
|
|
216
|
+
billingUsage: 'Pay-As-You-Go',
|
|
217
|
+
billingSub: 'Plan',
|
|
218
|
+
billingSwitchToSub: 'Switch to Plan mode',
|
|
219
|
+
billingSwitchToUsage: 'Switch to Pay-As-You-Go mode',
|
|
204
220
|
granted: 'Granted',
|
|
205
221
|
toppedUp: 'Topped up',
|
|
206
222
|
unavailable: 'Account unavailable',
|
|
@@ -272,11 +288,15 @@ window.__ModuleLoader__.load({
|
|
|
272
288
|
routingPrimary: 'primary',
|
|
273
289
|
routingNoRecommendation: 'none available',
|
|
274
290
|
routingLoading: 'resolving routing…',
|
|
291
|
+
routeChipSmart: 'Smart',
|
|
292
|
+
routeChipManual: 'Manual',
|
|
293
|
+
routeChipModeSmart: 'Smart routing',
|
|
294
|
+
routeChipModeManual: 'Manual model',
|
|
295
|
+
routeChipWouldRoute: 'Routes to',
|
|
296
|
+
routeChipLastRoute: 'Last used',
|
|
297
|
+
routeChipReasonNone: 'All candidates unavailable',
|
|
298
|
+
routeChipFailOpen: 'Falls back to your selection when routing fails',
|
|
275
299
|
aliasesMissing: 'no aliases configured',
|
|
276
|
-
keypoolTitle: 'Key rotation',
|
|
277
|
-
keypoolRound: 'round-robin',
|
|
278
|
-
keypoolActiveTip: 'Currently active',
|
|
279
|
-
keypoolNextTip: 'Next target',
|
|
280
300
|
heroTitle: 'LLM Matrix Overview',
|
|
281
301
|
heroProviders: 'Active providers',
|
|
282
302
|
heroModels: 'Models',
|
|
@@ -306,6 +326,7 @@ window.__ModuleLoader__.load({
|
|
|
306
326
|
suiteTitle: 'Webkubor DSH Plugin Suite',
|
|
307
327
|
suiteDesc: 'One-click full agent development environment',
|
|
308
328
|
suiteActive: 'Active',
|
|
329
|
+
suiteUnknown: 'Undetectable',
|
|
309
330
|
suiteCopyInstall: 'Copy install',
|
|
310
331
|
suiteCopied: 'Command copied'
|
|
311
332
|
}
|
|
@@ -388,6 +409,14 @@ window.__ModuleLoader__.load({
|
|
|
388
409
|
'.dsh-llm-hub-subtext{font-size:11px;color:var(--dsw-alias-label-secondary);opacity:.8}',
|
|
389
410
|
'.dsh-llm-hub-warn{color:var(--dsw-alias-state-warn-primary);font-size:11px}',
|
|
390
411
|
'.dsh-llm-hub-error{color:var(--dsw-alias-state-error-primary);font-size:11px}',
|
|
412
|
+
'.dsh-llm-hub-billing-capsule{display:inline-flex;align-items:center;background:var(--dsw-alias-fill-quaternary, rgba(125,125,125,0.08));border-radius:12px;padding:1.5px;gap:2px;user-select:none;margin-right:2px}',
|
|
413
|
+
'.dsh-llm-hub-billing-pill{font-size:10px;line-height:14px;padding:1px 6px;border-radius:10px;cursor:pointer;color:var(--dsw-alias-label-tertiary);transition:all .15s ease}',
|
|
414
|
+
'.dsh-llm-hub-billing-pill:hover{color:var(--dsw-alias-label-primary)}',
|
|
415
|
+
'.dsh-llm-hub-billing-pill.is-active{font-weight:600;background:var(--dsw-alias-fill-primary, #fff);box-shadow:0 1px 2px rgba(0,0,0,0.08)}',
|
|
416
|
+
'.dsh-llm-hub-billing-pill.is-active.is-usage{color:#2563eb}',
|
|
417
|
+
'.dsh-llm-hub-billing-pill.is-active.is-sub{color:#059669}',
|
|
418
|
+
'.dsh-llm-hub-key-chip{display:inline-flex;align-items:center;gap:3px;font-size:10.5px;padding:1px 5px;border-radius:4px;background:var(--dsw-alias-fill-quaternary, rgba(125,125,125,0.06));border:1px solid var(--dsw-alias-line-secondary, rgba(125,125,125,0.15));margin-right:4px}',
|
|
419
|
+
'.dsh-llm-hub-key-chip.is-down{opacity:.6;border-style:dashed}',
|
|
391
420
|
// 去掉粗糙灰底框,改用轻量排版标签
|
|
392
421
|
'.dsh-llm-hub-tag{display:inline-flex;align-items:center;gap:3px;padding:0 2px;',
|
|
393
422
|
'border:none;background:transparent;',
|
|
@@ -510,22 +539,34 @@ window.__ModuleLoader__.load({
|
|
|
510
539
|
'.dsh-llm-hub-routing__name{color:var(--dsw-alias-label-primary);font-weight:600}',
|
|
511
540
|
'.dsh-llm-hub-routing__suggestion{color:var(--dsw-alias-state-warn-primary);padding-top:2px}',
|
|
512
541
|
'.dsh-llm-hub-routing__reason{color:var(--dsw-alias-state-error-primary);padding-top:2px}',
|
|
513
|
-
//
|
|
514
|
-
'.dsh-llm-hub-
|
|
515
|
-
'
|
|
516
|
-
'border:.5px solid var(--dsw-alias-border-l1);background:transparent;',
|
|
517
|
-
'font
|
|
518
|
-
'.dsh-llm-hub-
|
|
519
|
-
'
|
|
520
|
-
'.dsh-llm-hub-
|
|
521
|
-
'.dsh-llm-hub-
|
|
522
|
-
'.dsh-llm-hub-
|
|
523
|
-
'.dsh-llm-hub-
|
|
524
|
-
'
|
|
525
|
-
'
|
|
526
|
-
'
|
|
527
|
-
'.dsh-llm-hub-
|
|
528
|
-
'.dsh-llm-hub-
|
|
542
|
+
// composer 快切 chip(conversation.input.left)
|
|
543
|
+
'.dsh-llm-hub-route{position:relative;display:inline-flex}',
|
|
544
|
+
'.dsh-llm-hub-route__btn{box-sizing:border-box;display:inline-flex;align-items:center;gap:4px;height:22px;',
|
|
545
|
+
'padding:0 8px;border-radius:6px;border:.5px solid var(--dsw-alias-border-l1);background:transparent;cursor:pointer;',
|
|
546
|
+
'font:inherit;font-size:11px;color:var(--dsw-alias-label-secondary);transition:all .15s ease;white-space:nowrap}',
|
|
547
|
+
'.dsh-llm-hub-route__btn:hover{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);',
|
|
548
|
+
'background:var(--dsw-alias-bg-layer-2)}',
|
|
549
|
+
'.dsh-llm-hub-route__btn.is-smart{border-color:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary)}',
|
|
550
|
+
'.dsh-llm-hub-route__btn.is-busy{opacity:.55;cursor:progress}',
|
|
551
|
+
'.dsh-llm-hub-route__mark{font-size:11px;line-height:1}',
|
|
552
|
+
'.dsh-llm-hub-route__menu{position:absolute;bottom:calc(100% + 6px);left:0;z-index:60;min-width:240px;',
|
|
553
|
+
'padding:8px;border-radius:8px;border:.5px solid var(--dsw-alias-border-l2);',
|
|
554
|
+
'background:var(--dsw-alias-bg-layer-1,rgba(15,15,15,.96));backdrop-filter:blur(14px);',
|
|
555
|
+
'box-shadow:0 8px 24px rgba(0,0,0,.28);display:flex;flex-direction:column;gap:8px;font-size:11px}',
|
|
556
|
+
'.dsh-llm-hub-route__modes{display:flex;gap:4px}',
|
|
557
|
+
'.dsh-llm-hub-route__mode{flex:1;padding:4px 6px;border-radius:5px;border:.5px solid var(--dsw-alias-border-l1);',
|
|
558
|
+
'background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit;font-size:10.5px;transition:all .15s ease}',
|
|
559
|
+
'.dsh-llm-hub-route__mode.is-on{border-color:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary);',
|
|
560
|
+
'background:var(--dsw-alias-bg-layer-2)}',
|
|
561
|
+
'.dsh-llm-hub-route__group{display:flex;flex-direction:column;gap:3px;padding:6px 7px;border-radius:6px;',
|
|
562
|
+
'border:.5px solid var(--dsw-alias-border-l1);background:transparent;cursor:pointer;text-align:left;',
|
|
563
|
+
'font:inherit;color:var(--dsw-alias-label-primary);transition:all .15s ease}',
|
|
564
|
+
'.dsh-llm-hub-route__group:hover{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2)}',
|
|
565
|
+
'.dsh-llm-hub-route__group.is-active{border-color:var(--dsw-alias-state-warn-primary)}',
|
|
566
|
+
'.dsh-llm-hub-route__group-top{display:flex;align-items:center;justify-content:space-between;gap:6px}',
|
|
567
|
+
'.dsh-llm-hub-route__group-name{font-weight:600;font-size:11px}',
|
|
568
|
+
'.dsh-llm-hub-route__state{font-size:10px;color:var(--dsw-alias-label-secondary);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
569
|
+
'.dsh-llm-hub-route__hint{font-size:10px;color:var(--dsw-alias-label-tertiary)}',
|
|
529
570
|
// Hero 全景统计大横条
|
|
530
571
|
'.dsh-llm-hub-hero{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:6px;margin:12px 0 6px 0}',
|
|
531
572
|
'.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)}',
|
|
@@ -574,6 +615,7 @@ window.__ModuleLoader__.load({
|
|
|
574
615
|
'.dsh-suite-card-bottom{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-top:auto;font-size:10.5px}',
|
|
575
616
|
'.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}',
|
|
576
617
|
'.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)}',
|
|
618
|
+
'.dsh-suite-badge-unknown{display:inline-flex;align-items:center;gap:4px;color:var(--dsw-alias-label-tertiary);font-weight:550;font-size:10.5px}',
|
|
577
619
|
'.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}',
|
|
578
620
|
'.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)}',
|
|
579
621
|
'.dsh-suite-link{color:var(--dsw-alias-label-tertiary);text-decoration:none;font-size:10px;display:inline-flex;align-items:center}',
|
|
@@ -722,13 +764,15 @@ window.__ModuleLoader__.load({
|
|
|
722
764
|
if (body == null || body.ok !== true) {
|
|
723
765
|
throw new Error(typeof body?.error === 'string' ? body.error : `HTTP ${response.status}`)
|
|
724
766
|
}
|
|
725
|
-
setState({
|
|
767
|
+
setState((prev) => ({
|
|
726
768
|
status: 'ok',
|
|
727
769
|
isAvailable: body.isAvailable === true,
|
|
728
770
|
balances: Array.isArray(body.balances) ? body.balances : [],
|
|
729
771
|
apiKey: typeof body.apiKey === 'string' ? body.apiKey : undefined,
|
|
730
|
-
keyMasked: typeof body.keyMasked === 'string' ? body.keyMasked : undefined
|
|
731
|
-
|
|
772
|
+
keyMasked: typeof body.keyMasked === 'string' ? body.keyMasked : undefined,
|
|
773
|
+
keys: Array.isArray(body.keys) ? body.keys : undefined,
|
|
774
|
+
billingMode: typeof body.billingMode === 'string' ? body.billingMode : (prev.billingMode || 'usage')
|
|
775
|
+
}))
|
|
732
776
|
})
|
|
733
777
|
.catch((error) => {
|
|
734
778
|
setState({ status: 'error', error: error instanceof Error ? error.message : String(error) })
|
|
@@ -809,9 +853,31 @@ window.__ModuleLoader__.load({
|
|
|
809
853
|
if (primary.granted !== '0' && primary.granted !== '0.00') breakdown.push(`${t('granted')} ${symbol}${primary.granted}`)
|
|
810
854
|
if (primary.toppedUp !== '0' && primary.toppedUp !== '0.00') breakdown.push(`${t('toppedUp')} ${symbol}${primary.toppedUp}`)
|
|
811
855
|
|
|
856
|
+
const mode = state.billingMode || 'usage'
|
|
857
|
+
const switchMode = (newMode) => {
|
|
858
|
+
setState((prev) => ({ ...prev, billingMode: newMode }))
|
|
859
|
+
fetch(BILLING_MODE_URL, {
|
|
860
|
+
method: 'POST',
|
|
861
|
+
headers: { 'content-type': 'application/json' },
|
|
862
|
+
body: JSON.stringify({ provider: 'deepseek', mode: newMode })
|
|
863
|
+
}).catch(() => {})
|
|
864
|
+
}
|
|
865
|
+
|
|
812
866
|
return h('div', { className: 'dsh-llm-hub-card' },
|
|
813
867
|
h('div', { className: 'dsh-llm-hub-card__meta' },
|
|
814
868
|
h('div', { className: 'dsh-llm-hub-card__tags' },
|
|
869
|
+
h('span', { className: 'dsh-llm-hub-billing-capsule' },
|
|
870
|
+
h('span', {
|
|
871
|
+
className: 'dsh-llm-hub-billing-pill ' + (mode === 'usage' ? 'is-active is-usage' : ''),
|
|
872
|
+
title: mode === 'usage' ? t('billingSwitchToSub') : t('billingSwitchToUsage'),
|
|
873
|
+
onClick: () => switchMode('usage')
|
|
874
|
+
}, t('billingUsage')),
|
|
875
|
+
h('span', {
|
|
876
|
+
className: 'dsh-llm-hub-billing-pill ' + (mode === 'subscription' ? 'is-active is-sub' : ''),
|
|
877
|
+
title: mode === 'subscription' ? t('billingSwitchToUsage') : t('billingSwitchToSub'),
|
|
878
|
+
onClick: () => switchMode('subscription')
|
|
879
|
+
}, t('billingSub'))
|
|
880
|
+
),
|
|
815
881
|
h('span', { className: 'dsh-llm-hub-balance-label' }, t('balance')),
|
|
816
882
|
h('span', { className: 'dsh-llm-hub-amount' + (warning?.level === 'low' ? ' is-low' : '') }, `${symbol}${primary.total}`),
|
|
817
883
|
h('span', { className: 'dsh-llm-hub-meta-sep' }, '·'),
|
|
@@ -841,10 +907,64 @@ window.__ModuleLoader__.load({
|
|
|
841
907
|
onClick: load
|
|
842
908
|
}, h(RefreshIcon, { spinning: isRefreshing }))
|
|
843
909
|
)
|
|
844
|
-
)
|
|
910
|
+
),
|
|
911
|
+
state.keys && state.keys.length > 1
|
|
912
|
+
? h('div', { className: 'dsh-llm-hub-card__actions', style: { paddingTop: '3px' } },
|
|
913
|
+
h('div', { className: 'dsh-llm-hub-card__tags' },
|
|
914
|
+
state.keys.map((k, idx) => h('span', {
|
|
915
|
+
key: k.ref,
|
|
916
|
+
className: 'dsh-llm-hub-key-chip' + (k.isAvailable ? '' : ' is-down'),
|
|
917
|
+
title: `${k.ref}: ${k.isAvailable ? '正常' : (k.error || '不可用')}`
|
|
918
|
+
}, `Key ${idx + 1} (${k.keyMasked}): ${k.isAvailable ? (k.currency === 'USD' ? '$' : '¥') + k.total : '⚠️ 余额不足'}`))
|
|
919
|
+
)
|
|
920
|
+
)
|
|
921
|
+
: null
|
|
845
922
|
)
|
|
846
923
|
}
|
|
847
924
|
|
|
925
|
+
function detectBillingMode(profile, status, balance) {
|
|
926
|
+
if (profile?.billingMode === 'usage' || profile?.billingMode === 'subscription') {
|
|
927
|
+
return profile.billingMode
|
|
928
|
+
}
|
|
929
|
+
if (status?.billingMode === 'usage' || status?.billingMode === 'subscription') {
|
|
930
|
+
return status.billingMode
|
|
931
|
+
}
|
|
932
|
+
if (balance && typeof balance === 'object') {
|
|
933
|
+
if (balance.kind === 'plan' || balance.kind === 'quota') return 'subscription'
|
|
934
|
+
if (Array.isArray(balance.items)) {
|
|
935
|
+
for (const item of balance.items) {
|
|
936
|
+
if (item?.resetsAt) return 'subscription'
|
|
937
|
+
if (typeof item?.weeklyPercent === 'number') return 'subscription'
|
|
938
|
+
if (item?.label && /(5h|hour|hourly|week|month|day|分|时|天|周|月)/i.test(item.label)) return 'subscription'
|
|
939
|
+
if (typeof item?.percent === 'number' && balance.meaning) return 'subscription'
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const providerName = String(profile?.provider || profile?.id || status?.provider || '').toLowerCase()
|
|
944
|
+
const baseURL = String(status?.baseURL || profile?.baseURL || '').toLowerCase()
|
|
945
|
+
|
|
946
|
+
// 特殊套餐/订阅服务商智能识别(如 OpenCode Go、各类 Coding Plan 等)
|
|
947
|
+
if (providerName.includes('opencode') || baseURL.includes('opencode.ai') || providerName.includes('coding-plan')) {
|
|
948
|
+
if (balance?.kind !== 'cash' && balance?.kind !== 'deepseek') {
|
|
949
|
+
return 'subscription'
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// 中转站(无定时限额的聚合/反代如 modelgo, relay, proxy, oneapi 等)基本都是按量付费
|
|
954
|
+
if (providerName.includes('modelgo') || providerName.includes('relay') || providerName.includes('proxy') || providerName.includes('oneapi') || providerName.includes('newapi') || providerName.includes('gateway')) {
|
|
955
|
+
return 'usage'
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// 厂商原厂域名或现金余额默认按量付费
|
|
959
|
+
if (baseURL.includes('deepseek.com') || baseURL.includes('bigmodel.cn') || baseURL.includes('openai.com') || baseURL.includes('anthropic.com') || baseURL.includes('volces.com') || baseURL.includes('moonshot.cn') || baseURL.includes('minimax.chat') || baseURL.includes('baichuan-ai.com')) {
|
|
960
|
+
return 'usage'
|
|
961
|
+
}
|
|
962
|
+
if (balance?.kind === 'cash' || balance?.kind === 'deepseek') {
|
|
963
|
+
return 'usage'
|
|
964
|
+
}
|
|
965
|
+
return 'usage'
|
|
966
|
+
}
|
|
967
|
+
|
|
848
968
|
/**
|
|
849
969
|
* 把 host 半的余额信封折成一行短文本;未加载 / 不支持时返回 null。
|
|
850
970
|
* reason 原样透传,因为 host 半保证它一定是人话("未填写服务地址,查不了余额"、
|
|
@@ -946,6 +1066,7 @@ window.__ModuleLoader__.load({
|
|
|
946
1066
|
const [copied, setCopied] = React.useState(false)
|
|
947
1067
|
const [balance, setBalance] = React.useState(null)
|
|
948
1068
|
const [warning, setWarning] = React.useState(null)
|
|
1069
|
+
const [billingModeOverride, setBillingModeOverride] = React.useState(null)
|
|
949
1070
|
// 被摘掉的分组,在它自己的卡片上直接标出来 —— 动作条里一枚小片,不占额外行。
|
|
950
1071
|
const availability = props.availability
|
|
951
1072
|
const availabilityState = availability === undefined
|
|
@@ -1068,8 +1189,30 @@ window.__ModuleLoader__.load({
|
|
|
1068
1189
|
h('span', { className: 'dsh-llm-hub-error' }, typeof status.error === 'string' ? status.error : t('statusFail'))))
|
|
1069
1190
|
}
|
|
1070
1191
|
|
|
1192
|
+
const currentBillingMode = billingModeOverride || status.billingMode || detectBillingMode(props.provider, status, balance)
|
|
1193
|
+
const switchBillingMode = (newMode) => {
|
|
1194
|
+
setBillingModeOverride(newMode)
|
|
1195
|
+
fetch(BILLING_MODE_URL, {
|
|
1196
|
+
method: 'POST',
|
|
1197
|
+
headers: { 'content-type': 'application/json' },
|
|
1198
|
+
body: JSON.stringify({ provider, mode: newMode })
|
|
1199
|
+
}).catch(() => {})
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1071
1202
|
// 元信息标签组(不重复展示外层已有的大标题,只呈现核心事实)
|
|
1072
1203
|
const tags = []
|
|
1204
|
+
tags.push(h('span', { key: 'billing-capsule', className: 'dsh-llm-hub-billing-capsule' },
|
|
1205
|
+
h('span', {
|
|
1206
|
+
className: 'dsh-llm-hub-billing-pill ' + (currentBillingMode === 'usage' ? 'is-active is-usage' : ''),
|
|
1207
|
+
title: currentBillingMode === 'usage' ? t('billingSwitchToSub') : t('billingSwitchToUsage'),
|
|
1208
|
+
onClick: () => switchBillingMode('usage')
|
|
1209
|
+
}, t('billingUsage')),
|
|
1210
|
+
h('span', {
|
|
1211
|
+
className: 'dsh-llm-hub-billing-pill ' + (currentBillingMode === 'subscription' ? 'is-active is-sub' : ''),
|
|
1212
|
+
title: currentBillingMode === 'subscription' ? t('billingSwitchToUsage') : t('billingSwitchToSub'),
|
|
1213
|
+
onClick: () => switchBillingMode('subscription')
|
|
1214
|
+
}, t('billingSub'))
|
|
1215
|
+
))
|
|
1073
1216
|
tags.push(h('span', { key: 'count', className: 'dsh-llm-hub-tag' }, `${t('configuredModels')} ${status.modelCount} ${t('modelsUnit')}`))
|
|
1074
1217
|
if (typeof status.api === 'string' && status.api !== '') {
|
|
1075
1218
|
const shortApi = status.api.replace('-messages', '').replace('-completions', '')
|
|
@@ -1420,49 +1563,103 @@ window.__ModuleLoader__.load({
|
|
|
1420
1563
|
}
|
|
1421
1564
|
|
|
1422
1565
|
/**
|
|
1423
|
-
*
|
|
1566
|
+
* composer 路由快切 chip(1.5.0 智能路由组)。
|
|
1567
|
+
*
|
|
1568
|
+
* 挂在 `conversation.input.left`(composer 工具行左端,追加型 list slot):
|
|
1569
|
+
* · 未配置任何路由组时整个 chip 不渲染 —— 存在即有意义;
|
|
1570
|
+
* · 关起时一行小字:⚡智能 · 公司(smart 模式)/ 🧭 · 公司(manual);
|
|
1571
|
+
* · 点开是浮层:模式二选一(智能路由 = 接管,实际路由按额度自动切;
|
|
1572
|
+
* 手动 = 尊重你在模型下拉里选的),下面是组列表(每组显示「将路由到
|
|
1573
|
+
* 哪个」与可用状态),点组即切、立即持久化。
|
|
1574
|
+
*
|
|
1575
|
+
* 数据流:GET /routing/state(挂载 + 每 30s 轮询 + 操作后重拉),
|
|
1576
|
+
* 写操作 POST /routing/select。chip 显示「最近实际路由」—— smart 模式
|
|
1577
|
+
* 下模型下拉显示什么已经不重要,这里才是事实。
|
|
1424
1578
|
*/
|
|
1425
|
-
function
|
|
1579
|
+
function SmartRouteChip(props) {
|
|
1426
1580
|
const t = typeof props.t === 'function' ? props.t : fallbackT
|
|
1427
|
-
const [
|
|
1428
|
-
React.
|
|
1429
|
-
|
|
1430
|
-
|
|
1581
|
+
const [state, setState] = React.useState(null)
|
|
1582
|
+
const [open, setOpen] = React.useState(false)
|
|
1583
|
+
const [busy, setBusy] = React.useState(false)
|
|
1584
|
+
const query = React.useCallback(() => {
|
|
1585
|
+
fetch(ROUTING_STATE_URL, { headers: { accept: 'application/json' } })
|
|
1431
1586
|
.then((res) => res.json())
|
|
1432
|
-
.then((body) => { if (
|
|
1433
|
-
.catch(() => {
|
|
1434
|
-
return () => { alive = false }
|
|
1587
|
+
.then((body) => { if (body !== null && typeof body === 'object' && body.ok === true) setState(body) })
|
|
1588
|
+
.catch(() => {})
|
|
1435
1589
|
}, [])
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1590
|
+
React.useEffect(() => {
|
|
1591
|
+
query()
|
|
1592
|
+
const interval = setInterval(query, 30000)
|
|
1593
|
+
return () => clearInterval(interval)
|
|
1594
|
+
}, [query])
|
|
1595
|
+
// 一个组都没有:chip 无事可做,不渲染(配置在 settings.yaml 里加)。
|
|
1596
|
+
if (state === null || !Array.isArray(state.groups) || state.groups.length === 0) return null
|
|
1439
1597
|
ensureStyle()
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1598
|
+
const activeGroup = state.groups.find((group) => group.id === state.activeGroup) ?? state.groups[0]
|
|
1599
|
+
const isSmart = state.mode === 'smart'
|
|
1600
|
+
const chipClass = [
|
|
1601
|
+
'dsh-llm-hub-route__btn',
|
|
1602
|
+
isSmart ? 'is-smart' : '',
|
|
1603
|
+
busy ? 'is-busy' : ''
|
|
1604
|
+
].filter(Boolean).join(' ')
|
|
1605
|
+
const label = (activeGroup && (activeGroup.label ?? activeGroup.id)) ?? ''
|
|
1606
|
+
const chipTitle = isSmart
|
|
1607
|
+
? `${t('routeChipModeSmart')} · ${label} —— ${state.lastDecision?.to ? `${t('routeChipLastRoute')}: ${state.lastDecision.to.provider}/${state.lastDecision.to.model}` : t('routeChipFailOpen')}`
|
|
1608
|
+
: `${t('routeChipModeManual')} · ${label}`
|
|
1609
|
+
const select = async (patch) => {
|
|
1610
|
+
setBusy(true)
|
|
1611
|
+
fetch(ROUTING_SELECT_URL, {
|
|
1612
|
+
method: 'POST',
|
|
1613
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
1614
|
+
body: JSON.stringify(patch)
|
|
1615
|
+
})
|
|
1616
|
+
.then((res) => res.json())
|
|
1617
|
+
.then((body) => { if (body !== null && typeof body === 'object' && body.ok === true) setState(body) })
|
|
1618
|
+
.catch(() => {})
|
|
1619
|
+
.finally(() => { setBusy(false); query() })
|
|
1620
|
+
}
|
|
1621
|
+
const stateText = (group) => {
|
|
1622
|
+
if (group.wouldRoute) {
|
|
1623
|
+
const next = `${group.wouldRoute.provider}/${group.wouldRoute.model}`
|
|
1624
|
+
return group.reason !== null && group.reason !== undefined ? group.reason : `${t('routeChipWouldRoute')}: ${next}`
|
|
1625
|
+
}
|
|
1626
|
+
return group.reason ?? t('routeChipReasonNone')
|
|
1444
1627
|
}
|
|
1445
|
-
const
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1628
|
+
const menu = open ? h('div', { className: 'dsh-llm-hub-route__menu' },
|
|
1629
|
+
h('div', { className: 'dsh-llm-hub-route__modes' },
|
|
1630
|
+
h('button', {
|
|
1631
|
+
type: 'button',
|
|
1632
|
+
className: `dsh-llm-hub-route__mode${isSmart ? ' is-on' : ''}`,
|
|
1633
|
+
onClick: () => { if (!isSmart) select({ mode: 'smart' }) }
|
|
1634
|
+
}, `⚡ ${t('routeChipModeSmart')}`),
|
|
1635
|
+
h('button', {
|
|
1636
|
+
type: 'button',
|
|
1637
|
+
className: `dsh-llm-hub-route__mode${!isSmart ? ' is-on' : ''}`,
|
|
1638
|
+
onClick: () => { if (isSmart) select({ mode: 'manual' }) }
|
|
1639
|
+
}, t('routeChipModeManual'))),
|
|
1640
|
+
...state.groups.map((group) => h('button', {
|
|
1641
|
+
key: group.id,
|
|
1642
|
+
type: 'button',
|
|
1643
|
+
className: `dsh-llm-hub-route__group${group.id === state.activeGroup ? ' is-active' : ''}`,
|
|
1644
|
+
onClick: () => {
|
|
1645
|
+
const patch = { activeGroup: group.id }
|
|
1646
|
+
if (!isSmart) patch.mode = 'smart'
|
|
1647
|
+
select(patch)
|
|
1648
|
+
}
|
|
1451
1649
|
},
|
|
1452
|
-
h('span', { className: 'dsh-llm-hub-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
h('span', {
|
|
1464
|
-
|
|
1465
|
-
h('div', { className: 'dsh-llm-hub-keypool__rows' }, rows))
|
|
1650
|
+
h('span', { className: 'dsh-llm-hub-route__group-top' },
|
|
1651
|
+
h('span', { className: 'dsh-llm-hub-route__group-name' }, group.label ?? group.id),
|
|
1652
|
+
h('span', { className: 'dsh-llm-hub-route__state' }, stateText(group)))))) : null
|
|
1653
|
+
return h('div', { className: 'dsh-llm-hub-route' },
|
|
1654
|
+
h('button', {
|
|
1655
|
+
type: 'button',
|
|
1656
|
+
className: chipClass,
|
|
1657
|
+
title: chipTitle,
|
|
1658
|
+
onClick: () => { setOpen(!open); query() }
|
|
1659
|
+
},
|
|
1660
|
+
h('span', { className: 'dsh-llm-hub-route__mark' }, isSmart ? '⚡' : '🧭'),
|
|
1661
|
+
h('span', null, `${isSmart ? `${t('routeChipSmart')} · ` : ''}${label}`)),
|
|
1662
|
+
menu)
|
|
1466
1663
|
}
|
|
1467
1664
|
|
|
1468
1665
|
/**
|
|
@@ -1668,11 +1865,13 @@ window.__ModuleLoader__.load({
|
|
|
1668
1865
|
onClick: () => {
|
|
1669
1866
|
// 别写死我自己的 profile 名,也别只给一条 npm i —— 装进 node_modules
|
|
1670
1867
|
// 不等于接进 boot graph,少了 bundles 那行插件根本不会加载。
|
|
1868
|
+
// 第 2 步对 `dsh plugin add` 是自动的(装完会 reconcile bundles),
|
|
1869
|
+
// 只有手写 package.json 或从别处搬时才需要手动补,所以标成「如果没自动加」。
|
|
1671
1870
|
const text = [
|
|
1672
1871
|
`${meta.name} —— DSH 模型页增强:网关可达性探测、余额/配额、模型目录勾选写回配置。`,
|
|
1673
1872
|
'装到你的 DSH(profile 名按自己的改,默认 web):',
|
|
1674
1873
|
` 1) dsh plugin --profile web add ${meta.name}`,
|
|
1675
|
-
` 2)
|
|
1874
|
+
` 2) 确认 "${meta.name}" 在 ~/.dsh/profiles/web/package.json 的 dsh.profile.bundles 里(dsh plugin add 会自动加)`,
|
|
1676
1875
|
' 3) 重启 DSH',
|
|
1677
1876
|
typeof meta.homepage === 'string' ? `仓库:${meta.homepage}` : ''
|
|
1678
1877
|
].filter((line) => line !== '').join('\n')
|
|
@@ -1707,6 +1906,38 @@ window.__ModuleLoader__.load({
|
|
|
1707
1906
|
links)
|
|
1708
1907
|
}
|
|
1709
1908
|
|
|
1909
|
+
/**
|
|
1910
|
+
* 已激活的客户端插件 id 集合 —— 读宿主注入的 `window.__DSH_BOOT__.entries`。
|
|
1911
|
+
*
|
|
1912
|
+
* 为什么是这个源:`__DSH_BOOT__` 由 `dsh-client-modules` 在渲染 index 时注入,
|
|
1913
|
+
* 就是浏览器真正要拉取并执行的那份组合图。它在,插件就一定活着;它不在,
|
|
1914
|
+
* 无论 npm 装没装都不会加载。比「读 profile 的 package.json」更硬:后者只说明
|
|
1915
|
+
* 装过,**接没接进 boot graph 是另一回事**(少了 `dsh.profile.bundles` 那行,
|
|
1916
|
+
* 依赖装了也不加载),而那正是这套 Dock 想告诉人的事。
|
|
1917
|
+
*
|
|
1918
|
+
* 读不到就返回 null(不是空集合):分不清「一个都没装」和「读不到」时,
|
|
1919
|
+
* 宁可不做判定 —— 全部显示成未安装,是把探测失败谎报成事实。
|
|
1920
|
+
* @returns 已激活的 id 集合,或 null。
|
|
1921
|
+
*/
|
|
1922
|
+
function readActivatedPluginIds() {
|
|
1923
|
+
try {
|
|
1924
|
+
const boot = typeof window === 'undefined' ? undefined : window.__DSH_BOOT__
|
|
1925
|
+
if (boot === null || typeof boot !== 'object' || !Array.isArray(boot.entries)) return null
|
|
1926
|
+
return new Set(boot.entries.filter((entry) => entry && typeof entry.id === 'string').map((entry) => entry.id))
|
|
1927
|
+
} catch {
|
|
1928
|
+
return null
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
/** 一个家族条目是否已激活:按 id 与 aliases 逐个比对(scope 迁移过的包名都在 aliases 里)。 */
|
|
1933
|
+
function isSuiteItemActive(item, activated) {
|
|
1934
|
+
if (activated === null) return null
|
|
1935
|
+
for (const candidate of [item.id, ...(item.aliases ?? [])]) {
|
|
1936
|
+
if (activated.has(candidate)) return true
|
|
1937
|
+
}
|
|
1938
|
+
return false
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1710
1941
|
/**
|
|
1711
1942
|
* Webkubor DSH 扩展家族互导 Dock
|
|
1712
1943
|
*/
|
|
@@ -1733,8 +1964,7 @@ window.__ModuleLoader__.load({
|
|
|
1733
1964
|
icon: '🎛️',
|
|
1734
1965
|
desc: '多模型网关、余额配额、多Key轮换与极客大屏',
|
|
1735
1966
|
repo: 'https://github.com/webkubor/dsh-llm-hub',
|
|
1736
|
-
pkg: '@dsh-plugins/dsh-llm-hub'
|
|
1737
|
-
isCurrent: true
|
|
1967
|
+
pkg: '@dsh-plugins/dsh-llm-hub'
|
|
1738
1968
|
},
|
|
1739
1969
|
{
|
|
1740
1970
|
id: '@dsh-plugins/dsh-user-mirror',
|
|
@@ -1756,8 +1986,14 @@ window.__ModuleLoader__.load({
|
|
|
1756
1986
|
}
|
|
1757
1987
|
]
|
|
1758
1988
|
|
|
1989
|
+
// 判定在 render 里现读:`__DSH_BOOT__` 是宿主渲染 index 时注入的全局,之后
|
|
1990
|
+
// 不会再变(组合图变了要整页重载),所以不需要状态也不需要订阅。
|
|
1991
|
+
const activated = readActivatedPluginIds()
|
|
1992
|
+
|
|
1759
1993
|
const handleCopy = (pkg) => {
|
|
1760
|
-
|
|
1994
|
+
// 命令必须带 `--profile`:`dsh plugin` 是 pnpm 的透传层,profile 是
|
|
1995
|
+
// **必填**选项,漏了直接报错退出 —— 复制出去的命令得是能跑的。
|
|
1996
|
+
const cmd = `dsh plugin --profile web add ${pkg}`
|
|
1761
1997
|
if (navigator.clipboard) {
|
|
1762
1998
|
navigator.clipboard.writeText(cmd)
|
|
1763
1999
|
}
|
|
@@ -1775,7 +2011,7 @@ window.__ModuleLoader__.load({
|
|
|
1775
2011
|
),
|
|
1776
2012
|
h('div', { className: 'dsh-suite-cards' },
|
|
1777
2013
|
...suite.map((item) => {
|
|
1778
|
-
const
|
|
2014
|
+
const active = isSuiteItemActive(item, activated)
|
|
1779
2015
|
const isCopied = copiedPkg === item.pkg
|
|
1780
2016
|
|
|
1781
2017
|
return h('div', { key: item.id, className: 'dsh-suite-card' },
|
|
@@ -1790,17 +2026,21 @@ window.__ModuleLoader__.load({
|
|
|
1790
2026
|
),
|
|
1791
2027
|
h('div', { className: 'dsh-suite-card-desc' }, item.desc),
|
|
1792
2028
|
h('div', { className: 'dsh-suite-card-bottom' },
|
|
1793
|
-
|
|
2029
|
+
active === true ? (
|
|
1794
2030
|
h('span', { className: 'dsh-suite-badge-active' },
|
|
1795
2031
|
h('span', { className: 'dsh-suite-badge-active-dot' }),
|
|
1796
2032
|
t('suiteActive')
|
|
1797
2033
|
)
|
|
2034
|
+
) : active === null ? (
|
|
2035
|
+
// 读不到 boot graph(老宿主 / 非 web 载体):说「判定不了」,
|
|
2036
|
+
// 不说「没装」—— 后者是把探测失败当结论。
|
|
2037
|
+
h('span', { className: 'dsh-suite-badge-unknown' }, t('suiteUnknown'))
|
|
1798
2038
|
) : (
|
|
1799
2039
|
h('button', {
|
|
1800
2040
|
type: 'button',
|
|
1801
2041
|
className: 'dsh-suite-btn-action',
|
|
1802
2042
|
onClick: () => handleCopy(item.pkg),
|
|
1803
|
-
title: `安装命令: dsh plugin
|
|
2043
|
+
title: `安装命令: dsh plugin --profile web add ${item.pkg}`
|
|
1804
2044
|
}, isCopied ? `✓ ${t('suiteCopied')}` : `⚡ ${t('suiteCopyInstall')}`)
|
|
1805
2045
|
),
|
|
1806
2046
|
h('a', {
|
|
@@ -1993,13 +2233,6 @@ window.__ModuleLoader__.load({
|
|
|
1993
2233
|
locale: NS,
|
|
1994
2234
|
inject: () => ({ t, availability })
|
|
1995
2235
|
}, HealthCard))
|
|
1996
|
-
ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
|
|
1997
|
-
name: 'settings.models.footer',
|
|
1998
|
-
id: 'dsh-llm-hub-keypool',
|
|
1999
|
-
order: 73,
|
|
2000
|
-
locale: NS,
|
|
2001
|
-
inject: () => ({ t })
|
|
2002
|
-
}, KeyPoolCard))
|
|
2003
2236
|
ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
|
|
2004
2237
|
name: 'settings.models.footer',
|
|
2005
2238
|
id: 'dsh-llm-hub-routing',
|
|
@@ -2035,6 +2268,15 @@ window.__ModuleLoader__.load({
|
|
|
2035
2268
|
locale: NS,
|
|
2036
2269
|
inject: () => ({ t })
|
|
2037
2270
|
}, WebkuborSuiteDock))
|
|
2271
|
+
// composer 工具行左端的路由快切 chip —— 「切公司 / 切个人 + 智能开关」
|
|
2272
|
+
// 的高频操作留在聊天页,不用进设置页。list slot 必须带 id(同 footer 的教训)。
|
|
2273
|
+
ctx.slots.inject('conversation.input.left', () => ctx.slots.register({
|
|
2274
|
+
name: 'conversation.input.left',
|
|
2275
|
+
id: 'dsh-llm-hub-route-chip',
|
|
2276
|
+
order: 20,
|
|
2277
|
+
locale: NS,
|
|
2278
|
+
inject: () => ({ t })
|
|
2279
|
+
}, SmartRouteChip))
|
|
2038
2280
|
}
|
|
2039
2281
|
|
|
2040
2282
|
exports.apply = apply
|