@dsh-plugins/dsh-llm-hub 1.4.0 → 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 +131 -0
- package/lib/availability-store.js +168 -0
- package/lib/client.js +330 -11
- package/lib/index.js +536 -109
- package/lib/sorter.js +106 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -73,8 +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'
|
|
82
|
+
const BILLING_MODE_URL = '/api/dsh-llm-hub/billing-mode'
|
|
78
83
|
|
|
79
84
|
const LOCALES = {
|
|
80
85
|
zh: {
|
|
@@ -83,6 +88,10 @@ window.__ModuleLoader__.load({
|
|
|
83
88
|
refresh: '刷新余额',
|
|
84
89
|
retry: '重试',
|
|
85
90
|
noKey: '未配置 API Key',
|
|
91
|
+
billingUsage: '按量付费',
|
|
92
|
+
billingSub: '套餐',
|
|
93
|
+
billingSwitchToSub: '切换为套餐模式',
|
|
94
|
+
billingSwitchToUsage: '切换为按量付费模式',
|
|
86
95
|
noKeyHint: '在下方填入 DeepSeek API Key 后即可查询余额。',
|
|
87
96
|
granted: '赠送',
|
|
88
97
|
toppedUp: '充值',
|
|
@@ -155,6 +164,14 @@ window.__ModuleLoader__.load({
|
|
|
155
164
|
routingPrimary: '主力',
|
|
156
165
|
routingNoRecommendation: '全部不可用',
|
|
157
166
|
routingLoading: '路由解析中…',
|
|
167
|
+
routeChipSmart: '智能',
|
|
168
|
+
routeChipManual: '手动',
|
|
169
|
+
routeChipModeSmart: '智能路由',
|
|
170
|
+
routeChipModeManual: '手动模型',
|
|
171
|
+
routeChipWouldRoute: '将路由到',
|
|
172
|
+
routeChipLastRoute: '最近实际',
|
|
173
|
+
routeChipReasonNone: '候选全部不可用',
|
|
174
|
+
routeChipFailOpen: '路由失败时按原配置发出',
|
|
158
175
|
aliasesMissing: '尚未配置别名',
|
|
159
176
|
heroTitle: 'LLM 矩阵概览',
|
|
160
177
|
heroProviders: '活跃提供方',
|
|
@@ -185,6 +202,7 @@ window.__ModuleLoader__.load({
|
|
|
185
202
|
suiteTitle: 'Webkubor DSH 扩展家族',
|
|
186
203
|
suiteDesc: '一键构建完整智能体开发环境',
|
|
187
204
|
suiteActive: '已激活',
|
|
205
|
+
suiteUnknown: '判定不可用',
|
|
188
206
|
suiteCopyInstall: '复制安装',
|
|
189
207
|
suiteCopied: '已复制命令'
|
|
190
208
|
},
|
|
@@ -195,6 +213,10 @@ window.__ModuleLoader__.load({
|
|
|
195
213
|
retry: 'Retry',
|
|
196
214
|
noKey: 'No API key',
|
|
197
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',
|
|
198
220
|
granted: 'Granted',
|
|
199
221
|
toppedUp: 'Topped up',
|
|
200
222
|
unavailable: 'Account unavailable',
|
|
@@ -266,6 +288,14 @@ window.__ModuleLoader__.load({
|
|
|
266
288
|
routingPrimary: 'primary',
|
|
267
289
|
routingNoRecommendation: 'none available',
|
|
268
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',
|
|
269
299
|
aliasesMissing: 'no aliases configured',
|
|
270
300
|
heroTitle: 'LLM Matrix Overview',
|
|
271
301
|
heroProviders: 'Active providers',
|
|
@@ -296,6 +326,7 @@ window.__ModuleLoader__.load({
|
|
|
296
326
|
suiteTitle: 'Webkubor DSH Plugin Suite',
|
|
297
327
|
suiteDesc: 'One-click full agent development environment',
|
|
298
328
|
suiteActive: 'Active',
|
|
329
|
+
suiteUnknown: 'Undetectable',
|
|
299
330
|
suiteCopyInstall: 'Copy install',
|
|
300
331
|
suiteCopied: 'Command copied'
|
|
301
332
|
}
|
|
@@ -378,6 +409,14 @@ window.__ModuleLoader__.load({
|
|
|
378
409
|
'.dsh-llm-hub-subtext{font-size:11px;color:var(--dsw-alias-label-secondary);opacity:.8}',
|
|
379
410
|
'.dsh-llm-hub-warn{color:var(--dsw-alias-state-warn-primary);font-size:11px}',
|
|
380
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}',
|
|
381
420
|
// 去掉粗糙灰底框,改用轻量排版标签
|
|
382
421
|
'.dsh-llm-hub-tag{display:inline-flex;align-items:center;gap:3px;padding:0 2px;',
|
|
383
422
|
'border:none;background:transparent;',
|
|
@@ -500,6 +539,34 @@ window.__ModuleLoader__.load({
|
|
|
500
539
|
'.dsh-llm-hub-routing__name{color:var(--dsw-alias-label-primary);font-weight:600}',
|
|
501
540
|
'.dsh-llm-hub-routing__suggestion{color:var(--dsw-alias-state-warn-primary);padding-top:2px}',
|
|
502
541
|
'.dsh-llm-hub-routing__reason{color:var(--dsw-alias-state-error-primary);padding-top:2px}',
|
|
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)}',
|
|
503
570
|
// Hero 全景统计大横条
|
|
504
571
|
'.dsh-llm-hub-hero{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:6px;margin:12px 0 6px 0}',
|
|
505
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)}',
|
|
@@ -548,6 +615,7 @@ window.__ModuleLoader__.load({
|
|
|
548
615
|
'.dsh-suite-card-bottom{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-top:auto;font-size:10.5px}',
|
|
549
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}',
|
|
550
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}',
|
|
551
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}',
|
|
552
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)}',
|
|
553
621
|
'.dsh-suite-link{color:var(--dsw-alias-label-tertiary);text-decoration:none;font-size:10px;display:inline-flex;align-items:center}',
|
|
@@ -696,13 +764,15 @@ window.__ModuleLoader__.load({
|
|
|
696
764
|
if (body == null || body.ok !== true) {
|
|
697
765
|
throw new Error(typeof body?.error === 'string' ? body.error : `HTTP ${response.status}`)
|
|
698
766
|
}
|
|
699
|
-
setState({
|
|
767
|
+
setState((prev) => ({
|
|
700
768
|
status: 'ok',
|
|
701
769
|
isAvailable: body.isAvailable === true,
|
|
702
770
|
balances: Array.isArray(body.balances) ? body.balances : [],
|
|
703
771
|
apiKey: typeof body.apiKey === 'string' ? body.apiKey : undefined,
|
|
704
|
-
keyMasked: typeof body.keyMasked === 'string' ? body.keyMasked : undefined
|
|
705
|
-
|
|
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
|
+
}))
|
|
706
776
|
})
|
|
707
777
|
.catch((error) => {
|
|
708
778
|
setState({ status: 'error', error: error instanceof Error ? error.message : String(error) })
|
|
@@ -783,9 +853,31 @@ window.__ModuleLoader__.load({
|
|
|
783
853
|
if (primary.granted !== '0' && primary.granted !== '0.00') breakdown.push(`${t('granted')} ${symbol}${primary.granted}`)
|
|
784
854
|
if (primary.toppedUp !== '0' && primary.toppedUp !== '0.00') breakdown.push(`${t('toppedUp')} ${symbol}${primary.toppedUp}`)
|
|
785
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
|
+
|
|
786
866
|
return h('div', { className: 'dsh-llm-hub-card' },
|
|
787
867
|
h('div', { className: 'dsh-llm-hub-card__meta' },
|
|
788
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
|
+
),
|
|
789
881
|
h('span', { className: 'dsh-llm-hub-balance-label' }, t('balance')),
|
|
790
882
|
h('span', { className: 'dsh-llm-hub-amount' + (warning?.level === 'low' ? ' is-low' : '') }, `${symbol}${primary.total}`),
|
|
791
883
|
h('span', { className: 'dsh-llm-hub-meta-sep' }, '·'),
|
|
@@ -815,10 +907,64 @@ window.__ModuleLoader__.load({
|
|
|
815
907
|
onClick: load
|
|
816
908
|
}, h(RefreshIcon, { spinning: isRefreshing }))
|
|
817
909
|
)
|
|
818
|
-
)
|
|
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
|
|
819
922
|
)
|
|
820
923
|
}
|
|
821
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
|
+
|
|
822
968
|
/**
|
|
823
969
|
* 把 host 半的余额信封折成一行短文本;未加载 / 不支持时返回 null。
|
|
824
970
|
* reason 原样透传,因为 host 半保证它一定是人话("未填写服务地址,查不了余额"、
|
|
@@ -920,6 +1066,7 @@ window.__ModuleLoader__.load({
|
|
|
920
1066
|
const [copied, setCopied] = React.useState(false)
|
|
921
1067
|
const [balance, setBalance] = React.useState(null)
|
|
922
1068
|
const [warning, setWarning] = React.useState(null)
|
|
1069
|
+
const [billingModeOverride, setBillingModeOverride] = React.useState(null)
|
|
923
1070
|
// 被摘掉的分组,在它自己的卡片上直接标出来 —— 动作条里一枚小片,不占额外行。
|
|
924
1071
|
const availability = props.availability
|
|
925
1072
|
const availabilityState = availability === undefined
|
|
@@ -1042,8 +1189,30 @@ window.__ModuleLoader__.load({
|
|
|
1042
1189
|
h('span', { className: 'dsh-llm-hub-error' }, typeof status.error === 'string' ? status.error : t('statusFail'))))
|
|
1043
1190
|
}
|
|
1044
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
|
+
|
|
1045
1202
|
// 元信息标签组(不重复展示外层已有的大标题,只呈现核心事实)
|
|
1046
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
|
+
))
|
|
1047
1216
|
tags.push(h('span', { key: 'count', className: 'dsh-llm-hub-tag' }, `${t('configuredModels')} ${status.modelCount} ${t('modelsUnit')}`))
|
|
1048
1217
|
if (typeof status.api === 'string' && status.api !== '') {
|
|
1049
1218
|
const shortApi = status.api.replace('-messages', '').replace('-completions', '')
|
|
@@ -1394,6 +1563,104 @@ window.__ModuleLoader__.load({
|
|
|
1394
1563
|
}
|
|
1395
1564
|
|
|
1396
1565
|
/**
|
|
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
|
+
* 下模型下拉显示什么已经不重要,这里才是事实。
|
|
1578
|
+
*/
|
|
1579
|
+
function SmartRouteChip(props) {
|
|
1580
|
+
const t = typeof props.t === 'function' ? props.t : fallbackT
|
|
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' } })
|
|
1586
|
+
.then((res) => res.json())
|
|
1587
|
+
.then((body) => { if (body !== null && typeof body === 'object' && body.ok === true) setState(body) })
|
|
1588
|
+
.catch(() => {})
|
|
1589
|
+
}, [])
|
|
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
|
|
1597
|
+
ensureStyle()
|
|
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')
|
|
1627
|
+
}
|
|
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
|
+
}
|
|
1649
|
+
},
|
|
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)
|
|
1663
|
+
}
|
|
1397
1664
|
|
|
1398
1665
|
/**
|
|
1399
1666
|
* 当前路由卡 —— 显示用户当前激活的 (provider, model) + 状态 + 推荐的 fallback。
|
|
@@ -1598,11 +1865,13 @@ window.__ModuleLoader__.load({
|
|
|
1598
1865
|
onClick: () => {
|
|
1599
1866
|
// 别写死我自己的 profile 名,也别只给一条 npm i —— 装进 node_modules
|
|
1600
1867
|
// 不等于接进 boot graph,少了 bundles 那行插件根本不会加载。
|
|
1868
|
+
// 第 2 步对 `dsh plugin add` 是自动的(装完会 reconcile bundles),
|
|
1869
|
+
// 只有手写 package.json 或从别处搬时才需要手动补,所以标成「如果没自动加」。
|
|
1601
1870
|
const text = [
|
|
1602
1871
|
`${meta.name} —— DSH 模型页增强:网关可达性探测、余额/配额、模型目录勾选写回配置。`,
|
|
1603
1872
|
'装到你的 DSH(profile 名按自己的改,默认 web):',
|
|
1604
1873
|
` 1) dsh plugin --profile web add ${meta.name}`,
|
|
1605
|
-
` 2)
|
|
1874
|
+
` 2) 确认 "${meta.name}" 在 ~/.dsh/profiles/web/package.json 的 dsh.profile.bundles 里(dsh plugin add 会自动加)`,
|
|
1606
1875
|
' 3) 重启 DSH',
|
|
1607
1876
|
typeof meta.homepage === 'string' ? `仓库:${meta.homepage}` : ''
|
|
1608
1877
|
].filter((line) => line !== '').join('\n')
|
|
@@ -1637,6 +1906,38 @@ window.__ModuleLoader__.load({
|
|
|
1637
1906
|
links)
|
|
1638
1907
|
}
|
|
1639
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
|
+
|
|
1640
1941
|
/**
|
|
1641
1942
|
* Webkubor DSH 扩展家族互导 Dock
|
|
1642
1943
|
*/
|
|
@@ -1663,8 +1964,7 @@ window.__ModuleLoader__.load({
|
|
|
1663
1964
|
icon: '🎛️',
|
|
1664
1965
|
desc: '多模型网关、余额配额、多Key轮换与极客大屏',
|
|
1665
1966
|
repo: 'https://github.com/webkubor/dsh-llm-hub',
|
|
1666
|
-
pkg: '@dsh-plugins/dsh-llm-hub'
|
|
1667
|
-
isCurrent: true
|
|
1967
|
+
pkg: '@dsh-plugins/dsh-llm-hub'
|
|
1668
1968
|
},
|
|
1669
1969
|
{
|
|
1670
1970
|
id: '@dsh-plugins/dsh-user-mirror',
|
|
@@ -1686,8 +1986,14 @@ window.__ModuleLoader__.load({
|
|
|
1686
1986
|
}
|
|
1687
1987
|
]
|
|
1688
1988
|
|
|
1989
|
+
// 判定在 render 里现读:`__DSH_BOOT__` 是宿主渲染 index 时注入的全局,之后
|
|
1990
|
+
// 不会再变(组合图变了要整页重载),所以不需要状态也不需要订阅。
|
|
1991
|
+
const activated = readActivatedPluginIds()
|
|
1992
|
+
|
|
1689
1993
|
const handleCopy = (pkg) => {
|
|
1690
|
-
|
|
1994
|
+
// 命令必须带 `--profile`:`dsh plugin` 是 pnpm 的透传层,profile 是
|
|
1995
|
+
// **必填**选项,漏了直接报错退出 —— 复制出去的命令得是能跑的。
|
|
1996
|
+
const cmd = `dsh plugin --profile web add ${pkg}`
|
|
1691
1997
|
if (navigator.clipboard) {
|
|
1692
1998
|
navigator.clipboard.writeText(cmd)
|
|
1693
1999
|
}
|
|
@@ -1705,7 +2011,7 @@ window.__ModuleLoader__.load({
|
|
|
1705
2011
|
),
|
|
1706
2012
|
h('div', { className: 'dsh-suite-cards' },
|
|
1707
2013
|
...suite.map((item) => {
|
|
1708
|
-
const
|
|
2014
|
+
const active = isSuiteItemActive(item, activated)
|
|
1709
2015
|
const isCopied = copiedPkg === item.pkg
|
|
1710
2016
|
|
|
1711
2017
|
return h('div', { key: item.id, className: 'dsh-suite-card' },
|
|
@@ -1720,17 +2026,21 @@ window.__ModuleLoader__.load({
|
|
|
1720
2026
|
),
|
|
1721
2027
|
h('div', { className: 'dsh-suite-card-desc' }, item.desc),
|
|
1722
2028
|
h('div', { className: 'dsh-suite-card-bottom' },
|
|
1723
|
-
|
|
2029
|
+
active === true ? (
|
|
1724
2030
|
h('span', { className: 'dsh-suite-badge-active' },
|
|
1725
2031
|
h('span', { className: 'dsh-suite-badge-active-dot' }),
|
|
1726
2032
|
t('suiteActive')
|
|
1727
2033
|
)
|
|
2034
|
+
) : active === null ? (
|
|
2035
|
+
// 读不到 boot graph(老宿主 / 非 web 载体):说「判定不了」,
|
|
2036
|
+
// 不说「没装」—— 后者是把探测失败当结论。
|
|
2037
|
+
h('span', { className: 'dsh-suite-badge-unknown' }, t('suiteUnknown'))
|
|
1728
2038
|
) : (
|
|
1729
2039
|
h('button', {
|
|
1730
2040
|
type: 'button',
|
|
1731
2041
|
className: 'dsh-suite-btn-action',
|
|
1732
2042
|
onClick: () => handleCopy(item.pkg),
|
|
1733
|
-
title: `安装命令: dsh plugin
|
|
2043
|
+
title: `安装命令: dsh plugin --profile web add ${item.pkg}`
|
|
1734
2044
|
}, isCopied ? `✓ ${t('suiteCopied')}` : `⚡ ${t('suiteCopyInstall')}`)
|
|
1735
2045
|
),
|
|
1736
2046
|
h('a', {
|
|
@@ -1958,6 +2268,15 @@ window.__ModuleLoader__.load({
|
|
|
1958
2268
|
locale: NS,
|
|
1959
2269
|
inject: () => ({ t })
|
|
1960
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))
|
|
1961
2280
|
}
|
|
1962
2281
|
|
|
1963
2282
|
exports.apply = apply
|