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

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 CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  本项目遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [1.3.3] - 2026-09-25
6
+
7
+ ### ✨ 订阅制配额可视化进度条与重置倒计时
8
+
9
+ - **📊 订阅制可视化进度条胶囊(Subscription Quota Gauge)**:
10
+ - 参考 `claude-usage-statusline` 经典轨道式设计,在 Provider 卡片中引入高质感平滑迷你进度条;
11
+ - 动态三色渐变指示:`> 50%` 充裕翡翠绿(`#10b981`)、`15%~50%` 预警金黄(`#f59e0b`)、`<= 15%` 告急珊瑚红(`#ef4444`);
12
+ - 同时支持主限额(如 MiniMax 5 小时滚动配额、智谱 5h 配额)与次级周限额的双重进度条并列展示;
13
+ - **⏱️ 重置时刻与倒计时精准指示(Reset Time Tracking)**:
14
+ - 在配额进度条后实时标注重置时刻(如 `(15:40 重置)`),悬浮卡片提供完整余量与周期说明;
15
+ - 适配器底层自动解析服务端返回的 `current_interval_end_time` / `next_reset_time` / `expire_time` 真实重置时刻。
16
+
17
+ ## [1.3.2] - 2026-09-24
18
+
19
+ ### 🐛 修复与健壮性提升
20
+
21
+ - **⚡ 解决 MiniMax 等大模型临时限额导致下拉永久死锁问题**:
22
+ - 核心逻辑解耦:重构可用性过滤判定,严格区分「致命凭据故障(`CREDENTIAL_MISSING` / `AUTH_REJECTED`)」与「临时配额/余额状态(`QUOTA_EXHAUSTED` / `BALANCE_EMPTY` / `PAYMENT_REQUIRED`)」;
23
+ - 临时配额用尽(如 MiniMax 5 小时滚动配额 / 周配额达到 100% 用量)仅在设置页与状态卡片进行友好 warning 预警,**不再从 `llm.listProviders()` 下拉菜单中强行抹除**;
24
+ - 彻底破除「额度用尽 → 下拉消失 → 无法选择该模型发请求 → 运行期遥测恢复机制永久无法触发」的死锁链条;
25
+ - **🔄 后台主动保鲜与自愈机制(Auto-healing Probe)**:
26
+ - 在插件生命周期内新增守护探针轮询(`TTL = 5m`),结合 `.unref()` 保证无副作用后台运行;
27
+ - 当 MiniMax 5 小时重置窗口或余额恢复后,后台探针自动感知并重置为 `available`,无需手动重启或进入设置页刷新。
28
+
5
29
  ## [1.3.1] - 2026-09-23
6
30
 
7
31
  ### ✨ UI 视觉升级与极客装配 Hub
package/lib/client.js CHANGED
@@ -187,7 +187,12 @@ window.__ModuleLoader__.load({
187
187
  usageClear: '清空记录',
188
188
  usageCleared: '已清空',
189
189
  usageClearConfirm: '确认清空?',
190
- usageNoData: '本月还没有记录'
190
+ usageNoData: '本月还没有记录',
191
+ suiteTitle: 'Webkubor DSH 扩展家族',
192
+ suiteDesc: '一键构建完整智能体开发环境',
193
+ suiteActive: '已激活',
194
+ suiteCopyInstall: '复制安装',
195
+ suiteCopied: '已复制命令'
191
196
  },
192
197
  en: {
193
198
  balance: 'Balance',
@@ -297,7 +302,12 @@ window.__ModuleLoader__.load({
297
302
  usageClear: 'Clear records',
298
303
  usageCleared: 'Cleared',
299
304
  usageClearConfirm: 'Confirm clear?',
300
- usageNoData: 'no calls this month yet'
305
+ usageNoData: 'no calls this month yet',
306
+ suiteTitle: 'Webkubor DSH Plugin Suite',
307
+ suiteDesc: 'One-click full agent development environment',
308
+ suiteActive: 'Active',
309
+ suiteCopyInstall: 'Copy install',
310
+ suiteCopied: 'Command copied'
301
311
  }
302
312
  }
303
313
 
@@ -537,6 +547,37 @@ window.__ModuleLoader__.load({
537
547
  '.dsh-llm-hub-latency-badge.is-fast{background:rgba(16,185,129,.08);color:var(--dsw-alias-state-success-primary,#10b981);border:.5px solid rgba(16,185,129,.2)}',
538
548
  '.dsh-llm-hub-latency-badge.is-normal{background:rgba(234,179,8,.08);color:var(--dsw-alias-state-warn-primary,#eab308);border:.5px solid rgba(234,179,8,.2)}',
539
549
  '.dsh-llm-hub-latency-badge.is-slow{background:rgba(239,68,68,.08);color:var(--dsw-alias-state-error-primary,#ef4444);border:.5px solid rgba(239,68,68,.2)}',
550
+ // 订阅制配额可视化进度条与重置倒计时(参考 claude-usage-statusline 规范)
551
+ '.dsh-llm-hub-quota-group{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap;vertical-align:middle}',
552
+ '.dsh-llm-hub-quota-bar{display:inline-flex;align-items:center;gap:4.5px;padding:1.5px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.03));border:.5px solid var(--dsw-alias-border-l1,rgba(255,255,255,.08));font-size:10.5px;line-height:1;transition:all .15s ease}',
553
+ '.dsh-llm-hub-quota-bar:hover{border-color:var(--dsw-alias-brand-primary,rgba(59,130,246,.4));background:var(--dsw-alias-bg-layer-3,rgba(255,255,255,.06))}',
554
+ '.dsh-llm-hub-quota-label{font-weight:600;color:var(--dsw-alias-label-primary);font-size:10px}',
555
+ '.dsh-llm-hub-quota-track{width:46px;height:4.5px;border-radius:2.5px;background:rgba(255,255,255,.12);overflow:hidden;position:relative;display:inline-block;vertical-align:middle}',
556
+ '.dsh-llm-hub-quota-fill{height:100%;border-radius:2.5px;display:block;transition:width .3s cubic-bezier(0.4,0,0.2,1)}',
557
+ '.dsh-llm-hub-quota-fill.is-high{background:linear-gradient(90deg,#10b981,#34d399)}',
558
+ '.dsh-llm-hub-quota-fill.is-mid{background:linear-gradient(90deg,#f59e0b,#fbbf24)}',
559
+ '.dsh-llm-hub-quota-fill.is-low{background:linear-gradient(90deg,#ef4444,#f87171)}',
560
+ '.dsh-llm-hub-quota-pct{font-variant-numeric:tabular-nums;font-weight:600;color:var(--dsw-alias-label-primary);font-size:10.5px}',
561
+ '.dsh-llm-hub-quota-time{font-size:9.5px;color:var(--dsw-alias-label-secondary);opacity:.8;font-variant-numeric:tabular-nums;display:inline-flex;align-items:center;margin-left:1px}',
562
+ // Webkubor 插件全家桶互导矩阵
563
+ '.dsh-suite-dock{margin-top:14px;padding:14px 16px;background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,0.03));border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,0.08));border-radius:10px;backdrop-filter:blur(10px)}',
564
+ '.dsh-suite-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;flex-wrap:wrap;gap:8px}',
565
+ '.dsh-suite-title{font-size:12px;font-weight:650;color:var(--dsw-alias-label-primary);margin:0;display:flex;align-items:center;gap:6px}',
566
+ '.dsh-suite-desc{font-size:10.5px;color:var(--dsw-alias-label-tertiary);letter-spacing:.2px}',
567
+ '.dsh-suite-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px}',
568
+ '.dsh-suite-card{display:flex;flex-direction:column;justify-content:space-between;padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-layer-3,rgba(255,255,255,0.02));border:1px solid var(--dsw-alias-border-l2,rgba(255,255,255,0.05));transition:all .2s cubic-bezier(0.16,1,0.3,1)}',
569
+ '.dsh-suite-card:hover{transform:translateY(-1px);border-color:var(--dsw-alias-brand-primary,#3b82f6);box-shadow:0 4px 14px rgba(0,0,0,0.15)}',
570
+ '.dsh-suite-card-top{display:flex;align-items:center;gap:6px;margin-bottom:4px}',
571
+ '.dsh-suite-card-icon{font-size:15px;line-height:1}',
572
+ '.dsh-suite-card-name{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);text-decoration:none}',
573
+ '.dsh-suite-card-desc{font-size:10.5px;color:var(--dsw-alias-label-secondary);line-height:1.4;margin-bottom:8px;flex:1}',
574
+ '.dsh-suite-card-bottom{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-top:auto;font-size:10.5px}',
575
+ '.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
+ '.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)}',
577
+ '.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
+ '.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
+ '.dsh-suite-link{color:var(--dsw-alias-label-tertiary);text-decoration:none;font-size:10px;display:inline-flex;align-items:center}',
580
+ '.dsh-suite-link:hover{color:var(--dsw-alias-label-primary)}'
540
581
  ].join('')
541
582
  document.head.appendChild(style)
542
583
  }
@@ -659,7 +700,9 @@ window.__ModuleLoader__.load({
659
700
  const verdict = Array.isArray(availabilityState.providers)
660
701
  ? availabilityState.providers.find((entry) => entry.provider === DEEPSEEK_PROVIDER)
661
702
  : undefined
662
- const hiddenFromDropdown = verdict !== undefined && verdict.state === 'unavailable'
703
+ const hiddenFromDropdown = Array.isArray(availabilityState.hidden)
704
+ ? availabilityState.hidden.includes(DEEPSEEK_PROVIDER)
705
+ : (verdict !== undefined && verdict.state === 'unavailable')
663
706
 
664
707
  const load = React.useCallback(() => {
665
708
  if (!keyConfigured) {
@@ -830,14 +873,44 @@ window.__ModuleLoader__.load({
830
873
  return `${t('balance')} ${symbol}${first.amount}${voucher}`
831
874
  }
832
875
  const meaning = balance.meaning === 'used' ? t('used') : t('remain')
833
- const parts = []
876
+ const isUsed = balance.meaning === 'used'
877
+ const bars = []
834
878
  for (const item of items.slice(0, 2)) {
835
- let piece = `${item.label} ${meaning} ${item.percent}%`
836
- if (typeof item.weeklyPercent === 'number') piece += ` · ${t('week')} ${meaning} ${item.weeklyPercent}%`
837
- parts.push(piece)
879
+ const remainPct = isUsed ? Math.max(0, 100 - (item.percent ?? 0)) : (item.percent ?? 0)
880
+ const fillPct = Math.min(Math.max(remainPct, 0), 100)
881
+ const levelClass = fillPct > 50 ? 'is-high' : (fillPct > 20 ? 'is-mid' : 'is-low')
882
+ const labelText = item.label || '5h'
883
+ const resetTimeNote = item.resetsAt ? `(${item.resetsAt} 重置)` : ''
884
+ bars.push(h('span', {
885
+ key: labelText,
886
+ className: 'dsh-llm-hub-quota-bar',
887
+ title: `${labelText} ${meaning} ${item.percent}%${resetTimeNote ? ` · ${resetTimeNote}` : ''}`
888
+ },
889
+ h('span', { className: 'dsh-llm-hub-quota-label' }, labelText),
890
+ h('span', { className: 'dsh-llm-hub-quota-track' },
891
+ h('span', { className: `dsh-llm-hub-quota-fill ${levelClass}`, style: { width: `${fillPct}%` } })
892
+ ),
893
+ h('span', { className: 'dsh-llm-hub-quota-pct' }, `${item.percent}%`),
894
+ resetTimeNote ? h('span', { className: 'dsh-llm-hub-quota-time' }, resetTimeNote) : null
895
+ ))
896
+ if (typeof item.weeklyPercent === 'number') {
897
+ const weeklyRemain = isUsed ? Math.max(0, 100 - item.weeklyPercent) : item.weeklyPercent
898
+ const weeklyFill = Math.min(Math.max(weeklyRemain, 0), 100)
899
+ const weeklyClass = weeklyFill > 50 ? 'is-high' : (weeklyFill > 20 ? 'is-mid' : 'is-low')
900
+ bars.push(h('span', {
901
+ key: 'week',
902
+ className: 'dsh-llm-hub-quota-bar',
903
+ title: `${t('week')} ${meaning} ${item.weeklyPercent}%`
904
+ },
905
+ h('span', { className: 'dsh-llm-hub-quota-label' }, t('week')),
906
+ h('span', { className: 'dsh-llm-hub-quota-track' },
907
+ h('span', { className: `dsh-llm-hub-quota-fill ${weeklyClass}`, style: { width: `${weeklyFill}%` } })
908
+ ),
909
+ h('span', { className: 'dsh-llm-hub-quota-pct' }, `${item.weeklyPercent}%`)
910
+ ))
911
+ }
838
912
  }
839
- const level = typeof balance.level === 'string' && balance.level.length > 0 ? `${balance.level} · ` : ''
840
- return `${t('planQuota')} ${level}${parts.join(' / ')}`
913
+ return h('span', { className: 'dsh-llm-hub-quota-group' }, bars)
841
914
  }
842
915
 
843
916
  /**
@@ -881,7 +954,9 @@ window.__ModuleLoader__.load({
881
954
  const verdict = Array.isArray(availabilityState.providers)
882
955
  ? availabilityState.providers.find((entry) => entry.provider === provider)
883
956
  : undefined
884
- const hiddenFromDropdown = verdict !== undefined && verdict.state === 'unavailable'
957
+ const hiddenFromDropdown = Array.isArray(availabilityState.hidden)
958
+ ? availabilityState.hidden.includes(provider)
959
+ : (verdict !== undefined && verdict.state === 'unavailable')
885
960
 
886
961
  React.useEffect(() => {
887
962
  if (provider === '' || status === null || status.ok !== true || !status.balanceAdapter) return undefined
@@ -1632,6 +1707,115 @@ window.__ModuleLoader__.load({
1632
1707
  links)
1633
1708
  }
1634
1709
 
1710
+ /**
1711
+ * Webkubor DSH 扩展家族互导 Dock
1712
+ */
1713
+ function WebkuborSuiteDock(props) {
1714
+ const t = typeof props.t === 'function' ? props.t : fallbackT
1715
+ const [copiedPkg, setCopiedPkg] = React.useState(null)
1716
+
1717
+ ensureStyle()
1718
+
1719
+ const suite = [
1720
+ {
1721
+ id: '@dsh-plugins/dsh-bloom-theme',
1722
+ aliases: ['dsh-bloom-theme', '@dsh-plugins/dsh-bloom-theme'],
1723
+ name: 'Bloom Theme',
1724
+ icon: '🌸',
1725
+ desc: '落霞 Bloom 极客质感毛玻璃主题与代码高亮',
1726
+ repo: 'https://github.com/webkubor/dsh-bloom-theme',
1727
+ pkg: '@dsh-plugins/dsh-bloom-theme'
1728
+ },
1729
+ {
1730
+ id: '@dsh-plugins/dsh-llm-hub',
1731
+ aliases: ['dsh-llm-hub', '@dsh-plugins/dsh-llm-hub'],
1732
+ name: 'LLM Hub',
1733
+ icon: '🎛️',
1734
+ desc: '多模型网关、余额配额、多Key轮换与极客大屏',
1735
+ repo: 'https://github.com/webkubor/dsh-llm-hub',
1736
+ pkg: '@dsh-plugins/dsh-llm-hub',
1737
+ isCurrent: true
1738
+ },
1739
+ {
1740
+ id: '@dsh-plugins/dsh-user-mirror',
1741
+ aliases: ['dsh-user-mirror', 'dsh-mirror', '@dsh-plugins/dsh-user-mirror'],
1742
+ name: 'User Mirror',
1743
+ icon: '🪞',
1744
+ desc: '用户角色数字画像、习惯偏好与记忆网络',
1745
+ repo: 'https://github.com/webkubor/dsh-mirror',
1746
+ pkg: '@dsh-plugins/dsh-user-mirror'
1747
+ },
1748
+ {
1749
+ id: '@dsh-plugins/dsh-env-inspector',
1750
+ aliases: ['dsh-env-inspector', '@dsh-plugins/dsh-env-inspector'],
1751
+ name: 'Env Inspector',
1752
+ icon: '🖥️',
1753
+ desc: '端口监听释放、CLI工具链与环境大屏',
1754
+ repo: 'https://github.com/webkubor/dsh-env-inspector',
1755
+ pkg: '@dsh-plugins/dsh-env-inspector'
1756
+ }
1757
+ ]
1758
+
1759
+ const handleCopy = (pkg) => {
1760
+ const cmd = `dsh plugin install ${pkg}`
1761
+ if (navigator.clipboard) {
1762
+ navigator.clipboard.writeText(cmd)
1763
+ }
1764
+ setCopiedPkg(pkg)
1765
+ setTimeout(() => setCopiedPkg(null), 2000)
1766
+ }
1767
+
1768
+ return h('div', { className: 'dsh-suite-dock' },
1769
+ h('div', { className: 'dsh-suite-head' },
1770
+ h('h3', { className: 'dsh-suite-title' },
1771
+ h('span', null, '🌟'),
1772
+ t('suiteTitle')
1773
+ ),
1774
+ h('span', { className: 'dsh-suite-desc' }, t('suiteDesc'))
1775
+ ),
1776
+ h('div', { className: 'dsh-suite-cards' },
1777
+ ...suite.map((item) => {
1778
+ const isInstalled = item.isCurrent
1779
+ const isCopied = copiedPkg === item.pkg
1780
+
1781
+ return h('div', { key: item.id, className: 'dsh-suite-card' },
1782
+ h('div', { className: 'dsh-suite-card-top' },
1783
+ h('span', { className: 'dsh-suite-card-icon' }, item.icon),
1784
+ h('a', {
1785
+ href: item.repo,
1786
+ target: '_blank',
1787
+ rel: 'noopener noreferrer',
1788
+ className: 'dsh-suite-card-name'
1789
+ }, item.name)
1790
+ ),
1791
+ h('div', { className: 'dsh-suite-card-desc' }, item.desc),
1792
+ h('div', { className: 'dsh-suite-card-bottom' },
1793
+ isInstalled ? (
1794
+ h('span', { className: 'dsh-suite-badge-active' },
1795
+ h('span', { className: 'dsh-suite-badge-active-dot' }),
1796
+ t('suiteActive')
1797
+ )
1798
+ ) : (
1799
+ h('button', {
1800
+ type: 'button',
1801
+ className: 'dsh-suite-btn-action',
1802
+ onClick: () => handleCopy(item.pkg),
1803
+ title: `安装命令: dsh plugin install ${item.pkg}`
1804
+ }, isCopied ? `✓ ${t('suiteCopied')}` : `⚡ ${t('suiteCopyInstall')}`)
1805
+ ),
1806
+ h('a', {
1807
+ href: item.repo,
1808
+ target: '_blank',
1809
+ rel: 'noopener noreferrer',
1810
+ className: 'dsh-suite-link'
1811
+ }, 'GitHub ↗')
1812
+ )
1813
+ )
1814
+ })
1815
+ )
1816
+ )
1817
+ }
1818
+
1635
1819
  /**
1636
1820
  * 可用性判定缓存 —— 所有会话共用一份(同一个 host、同一份设置)。
1637
1821
  *
@@ -1844,6 +2028,13 @@ window.__ModuleLoader__.load({
1844
2028
  locale: NS,
1845
2029
  inject: () => ({ t, availability })
1846
2030
  }, HubFooter))
2031
+ ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
2032
+ name: 'settings.models.footer',
2033
+ id: 'dsh-llm-hub-suite',
2034
+ order: 110,
2035
+ locale: NS,
2036
+ inject: () => ({ t })
2037
+ }, WebkuborSuiteDock))
1847
2038
  }
1848
2039
 
1849
2040
  exports.apply = apply
package/lib/index.js CHANGED
@@ -923,10 +923,21 @@ export function apply(ctx) {
923
923
  if (row === null || typeof row !== 'object') continue
924
924
  const percent = percentage(row.current_interval_remaining_percent)
925
925
  if (percent === undefined) continue
926
+ let resetsAt = undefined
927
+ const rawTime = row.current_interval_end_time ?? row.end_time ?? row.reset_time
928
+ if (typeof rawTime === 'number' && rawTime > 0) {
929
+ const date = new Date(rawTime > 1e11 ? rawTime : rawTime * 1000)
930
+ if (!isNaN(date.getTime())) {
931
+ resetsAt = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
932
+ }
933
+ } else if (typeof rawTime === 'string' && rawTime.trim() !== '') {
934
+ resetsAt = rawTime.trim()
935
+ }
926
936
  items.push({
927
937
  label: label(row.model_name) ?? 'plan',
928
938
  percent,
929
- weeklyPercent: percentage(row.current_weekly_remaining_percent)
939
+ weeklyPercent: percentage(row.current_weekly_remaining_percent),
940
+ resetsAt
930
941
  })
931
942
  }
932
943
  return { items }
@@ -953,7 +964,17 @@ export function apply(ctx) {
953
964
  if (tier === undefined) continue
954
965
  const percent = percentage(lim.percentage)
955
966
  if (percent === undefined) continue
956
- items.push({ label: tier, percent })
967
+ let resetsAt = undefined
968
+ const rawTime = lim.reset_time ?? lim.next_reset_time ?? body?.data?.expire_time
969
+ if (typeof rawTime === 'number' && rawTime > 0) {
970
+ const date = new Date(rawTime > 1e11 ? rawTime : rawTime * 1000)
971
+ if (!isNaN(date.getTime())) {
972
+ resetsAt = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
973
+ }
974
+ } else if (typeof rawTime === 'string' && rawTime.trim() !== '') {
975
+ resetsAt = rawTime.trim()
976
+ }
977
+ items.push({ label: tier, percent, resetsAt })
957
978
  }
958
979
  return { level: label(body?.data?.level), items }
959
980
  }
@@ -1490,11 +1511,29 @@ export function apply(ctx) {
1490
1511
  return [...merged.values()].sort((left, right) => left.provider.localeCompare(right.provider))
1491
1512
  }
1492
1513
 
1514
+ /**
1515
+ * 判定是否属于应从模型下拉彻底摘掉的严重凭据故障。
1516
+ *
1517
+ * ⚠️ 设计原则(避免死锁):
1518
+ * 只有确凿的凭据故障(CREDENTIAL_MISSING / AUTH_REJECTED / ACCOUNT_UNAVAILABLE)才从下拉框剔除。
1519
+ * 配额耗尽(QUOTA_EXHAUSTED)与现金余额为 0(BALANCE_EMPTY / PAYMENT_REQUIRED)属于临时限额/计费状态,
1520
+ * 绝不能从下拉框中剔除,否则会导致:
1521
+ * 1) 用户在界面上永久丢失模型入口;
1522
+ * 2) 5小时滚动配额或周配额恢复后,用户因无法选模型而无法触发运行期遥测恢复;
1523
+ * 3) 形成死锁。
1524
+ */
1525
+ const isDropDeadHidden = (verdict) => {
1526
+ if (verdict === null || typeof verdict !== 'object') return false
1527
+ if (verdict.state !== 'unavailable') return false
1528
+ const code = verdict.code
1529
+ return code === 'CREDENTIAL_MISSING' || code === 'AUTH_REJECTED' || code === 'ACCOUNT_UNAVAILABLE'
1530
+ }
1531
+
1493
1532
  /** 当前该从下拉里摘掉的 provider 集合。 */
1494
1533
  const hiddenProviders = () => {
1495
1534
  const hidden = new Set()
1496
1535
  for (const verdict of currentAvailability()) {
1497
- if (verdict.state === 'unavailable') hidden.add(verdict.provider)
1536
+ if (isDropDeadHidden(verdict)) hidden.add(verdict.provider)
1498
1537
  }
1499
1538
  return hidden
1500
1539
  }
@@ -1530,7 +1569,7 @@ export function apply(ctx) {
1530
1569
  checkedAt: lastProbeAt,
1531
1570
  probing: probeAll !== null,
1532
1571
  providers,
1533
- hidden: providers.filter((verdict) => verdict.state === 'unavailable').map((verdict) => verdict.provider)
1572
+ hidden: providers.filter(isDropDeadHidden).map((verdict) => verdict.provider)
1534
1573
  }
1535
1574
  }
1536
1575
 
@@ -1630,8 +1669,16 @@ export function apply(ctx) {
1630
1669
  const timer = setTimeout(() => {
1631
1670
  refreshAvailability().catch(() => {})
1632
1671
  }, 1500)
1633
- return () => clearTimeout(timer)
1634
- }, 'dsh-llm-hub: initial availability probe')
1672
+ timer.unref?.()
1673
+ const interval = setInterval(() => {
1674
+ ensureAvailabilityFresh()
1675
+ }, AVAILABILITY_TTL_MS)
1676
+ interval.unref?.()
1677
+ return () => {
1678
+ clearTimeout(timer)
1679
+ clearInterval(interval)
1680
+ }
1681
+ }, 'dsh-llm-hub: initial and periodic availability probe')
1635
1682
 
1636
1683
  // 把不可用的 provider 从 llm 服务的路由表里摘掉。
1637
1684
  //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plugins/dsh-llm-hub",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "DSH 模型管理层:网关可达性探测、模型目录拉取、DeepSeek + MiniMax/智谱/Moonshot/StepFun 余额与配额、本月用量统计、余额预警、Provider 健康看板,再把本机装好的 codex / claude / agy 注册成子代理(装了才出现)。零运行时依赖,不改动 DSH 安装。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",