dsh-modelscope-pool 0.10.1 → 0.10.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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/lib/index.js +116 -57
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -121,7 +121,7 @@
121
121
  ### 方式一:从 npm 安装(推荐)
122
122
 
123
123
  ```bash
124
- dsh plugin --profile web add dsh-modelscope-pool@0.10.0
124
+ dsh plugin --profile web add dsh-modelscope-pool@0.10.3
125
125
  ```
126
126
 
127
127
  一条命令搞定。装好后重启 App(或让插件重新加载),网关会在 `127.0.0.1:8899` 起来。
@@ -334,7 +334,7 @@ dsh-go 里更进一步:这个插件会把自己注册成一个叫 **魔搭 Key
334
334
  看看是不是被「手动禁用」了(`manual` 标记)。手动禁用不会被自动恢复流程放开,需要点「恢复」。
335
335
 
336
336
  **点「刷新额度」提示「魔搭 Key 池没有响应」?**
337
- App 要求点按在 3 秒内返回,而刷新额度要联网逐个查所有 Key 的额度,早就超过 3 秒了。0.10.1 起这几个慢操作(刷新额度 / 测试全部 / 重新拉取模型 / 添加 Key)改成先回一个「正在刷新额度」的等待态,结果异步回来,最多 60 秒。如果你还在 0.10.0 或更早,升级即可。
337
+ App 要求点按在 3 秒内返回,而刷新额度要联网逐个查所有 Key 的额度,早就超过 3 秒了。0.10.3 起这几个慢操作(刷新额度 / 测试全部 / 重新拉取模型 / 添加 Key)不再在点按里等:点一下立刻在**面板顶部**显示「正在刷新额度」,**3 秒后自动收起**(操作不会中断),跑完再把结果挂上去 3 秒。如果你还在 0.10.0 或更早,升级即可。
338
338
 
339
339
  **端口被占用?**
340
340
  改 `config.json` 的 `port`,同时把客户端和供应商配置里的地址一起改掉。
package/lib/index.js CHANGED
@@ -144,6 +144,48 @@ export function apply(ctx) {
144
144
  })
145
145
  let refreshPanel = () => {}
146
146
  let refreshStrip = () => {}
147
+
148
+ // 慢操作的顶部提示:写进 data.hint,3 秒后自动收起(后台任务不中断)。
149
+ // 必须放在这一层:buildData 在外面读它,放进 inject 回调里会变成未定义变量。
150
+ let notice = ''
151
+ let noticeUntil = 0
152
+ const noticeNow = () => (noticeUntil > Date.now() ? notice : '')
153
+ let noticeTimer = null
154
+ const kicks = new Set()
155
+ const setNotice = (text, ms = 3000) => {
156
+ notice = String(text ?? '')
157
+ noticeUntil = notice ? Date.now() + ms : 0
158
+ try {
159
+ refreshPanel()
160
+ } catch {}
161
+ try {
162
+ refreshStrip()
163
+ } catch {}
164
+ // 立刻推一次:点下就能看到,不用等下一次心跳。
165
+ for (const kick of kicks) {
166
+ try {
167
+ kick()
168
+ } catch {}
169
+ }
170
+ // 到点主动推一次,让提示正好在 3 秒收起,而不是等下一次 1.5 秒心跳。
171
+ if (noticeTimer) clearTimeout(noticeTimer)
172
+ noticeTimer = null
173
+ if (noticeUntil > 0) {
174
+ noticeTimer = setTimeout(() => {
175
+ noticeTimer = null
176
+ if (noticeUntil > Date.now()) return
177
+ try {
178
+ refreshPanel()
179
+ } catch {}
180
+ for (const kick of kicks) {
181
+ try {
182
+ kick()
183
+ } catch {}
184
+ }
185
+ }, ms + 60)
186
+ if (typeof noticeTimer.unref === 'function') noticeTimer.unref()
187
+ }
188
+ }
147
189
  // Key 级无感路由:模型不变,只换 Key。
148
190
  const router = createRouter({ logger })
149
191
 
@@ -300,14 +342,16 @@ const modelRowOf = (entry, current, t) => {
300
342
  const routing = router.snapshot()
301
343
  const routed =
302
344
  routing.blockedPairs > 0 ? ` ${t('routed', { n: routing.blockedPairs })}` : ''
345
+ const busy = noticeNow()
303
346
  const hint =
304
- s.total === 0
347
+ busy ||
348
+ (s.total === 0
305
349
  ? t('hintNoKeys')
306
350
  : s.cooling > 0
307
351
  ? `${t('hintCooling')}${routed}`
308
352
  : model
309
353
  ? `${t('hintModel', { model })}${routed}`
310
- : `${t('hintIdle')}${routed}`
354
+ : `${t('hintIdle')}${routed}`)
311
355
 
312
356
  // 扇形图:Key 池的状态分布(可用 / 额度不足 / 令牌无效 / 其他冷却)
313
357
  const order = [
@@ -515,6 +559,7 @@ const modelRowOf = (entry, current, t) => {
515
559
  title: t('poolTitle'),
516
560
  accent: 'teal',
517
561
  children: [
562
+ { type: 'callout', tone: 'info', text: { $: 'data.hint' } },
518
563
  {
519
564
  type: 'row',
520
565
  children: [
@@ -531,7 +576,6 @@ const modelRowOf = (entry, current, t) => {
531
576
  caption: { $: 'data.magic.brief' },
532
577
  tone: 'good',
533
578
  },
534
- { type: 'callout', tone: 'info', text: { $: 'data.hint' } },
535
579
  { type: 'text', text: { $: 'data.keyTitle' }, style: { size: 'title', weight: 'bold' } },
536
580
  { type: 'list', items: { $: 'data.keys' } },
537
581
  {
@@ -716,37 +760,52 @@ const modelRowOf = (entry, current, t) => {
716
760
  ready
717
761
  .then(() => {
718
762
  tick()
763
+ kicks.add(tick)
719
764
  const timer = setInterval(tick, 1500)
720
765
  if (typeof timer.unref === 'function') timer.unref()
721
- stop = () => clearInterval(timer)
766
+ stop = () => {
767
+ kicks.delete(tick)
768
+ clearInterval(timer)
769
+ }
722
770
  })
723
771
  .catch(() => {})
724
772
  let stop = () => {}
725
773
  return () => {
726
774
  stopped = true
775
+ kicks.delete(tick)
727
776
  stop()
728
777
  }
729
778
  }
730
779
 
731
- // 点按必须在 3 秒内返回,否则 App 会提示「没有响应」。
732
- // 刷新额度 / 测试全部 / 拉模型 / 校验 Key 都要联网,所以先回 pending,结果走延后效果。
733
- const defer = (label, work) => ({
734
- pending: { label, timeoutMs: 60_000 },
735
- result: async () => {
736
- const value = await work()
737
- refreshPanel()
738
- refreshStrip()
739
- return value
740
- },
741
- })
780
+ // 点按必须 3 秒内返回,否则 App 会提示「没有响应」。
781
+ // 联网的慢操作因此不在点按里等:先把一句话挂到面板顶部(data.hint),
782
+ // 3 秒后自动收起,真正的活儿在后台继续跑,跑完再挂一句结果,同样 3 秒收起。
783
+ const running = new Set()
784
+ const runInBackground = (key, label, work) => {
785
+ if (running.has(key)) return { toast: label }
786
+ running.add(key)
787
+ setNotice(label)
788
+ void (async () => {
789
+ let message = ''
790
+ try {
791
+ message = String((await work()) ?? '')
792
+ } catch (error) {
793
+ message = String(error?.message ?? error ?? '')
794
+ } finally {
795
+ running.delete(key)
796
+ }
797
+ setNotice(message)
798
+ })()
799
+ return { refresh: true }
800
+ }
742
801
 
743
802
  const panelActions = {
744
803
  refresh(scope, event) {
745
804
  const t = makeT(event?.locale)
746
- return defer(t('pendingRefresh'), async () => {
805
+ return runInBackground('refresh', t('pendingRefresh'), async () => {
747
806
  await refreshQuota(true)
748
807
  await refreshCatalog(true)
749
- return { toast: t('toastQuotaUpdated') }
808
+ return t('toastQuotaUpdated')
750
809
  })
751
810
  },
752
811
  async 'pick-model'(scope, event) {
@@ -759,37 +818,35 @@ const modelRowOf = (entry, current, t) => {
759
818
  },
760
819
  'test-all'(scope, event) {
761
820
  const t = makeT(event?.locale)
762
- const keys = pool ? [...pool.keys] : []
763
- if (keys.length === 0) return { toast: t('toastNoKeys') }
764
- // 注意不能用 /v1/models 验 Key:那个接口是公开的,什么令牌都返回 200。
765
- // 真正会校验令牌的是额度接口,而且查额度不扣魔粒。
766
- return defer(t('pendingTest'), async () => {
767
- const rows = await quota.refresh(
768
- keys.map((k) => ({ id: k.id, key: k.key, region: k.region })),
769
- { force: true },
770
- )
771
- for (const [i, row] of rows.entries()) {
772
- const k = keys[i]
773
- if (row.state?.region) await pool.setRegion(k.id, row.state.region)
774
- if (!row.state?.ok) {
775
- pool.markFailure(k.id, '令牌无效', String(row.state?.error ?? '').slice(0, 80))
776
- continue
777
- }
778
- if (row.state.available < QUOTA_FLOOR) {
779
- pool.markFailure(k.id, '额度不足', `剩余 ${row.state.available} 魔粒`, nextQuotaResetAt())
780
- continue
821
+ return runInBackground('test', t('pendingTest'), async () => {
822
+ const keys = pool ? [...pool.keys] : []
823
+ if (keys.length === 0) return t('toastNoKeys')
824
+ // 注意不能用 /v1/models 验 Key:那个接口是公开的,什么令牌都返回 200。
825
+ // 真正会校验令牌的是额度接口,而且查额度不扣魔粒。
826
+ const rows = await quota.refresh(
827
+ keys.map((k) => ({ id: k.id, key: k.key, region: k.region })),
828
+ { force: true },
829
+ )
830
+ for (const [i, row] of rows.entries()) {
831
+ const k = keys[i]
832
+ if (row.state?.region) await pool.setRegion(k.id, row.state.region)
833
+ if (!row.state?.ok) {
834
+ pool.markFailure(k.id, '令牌无效', String(row.state?.error ?? '').slice(0, 80))
835
+ continue
836
+ }
837
+ if (row.state.available < QUOTA_FLOOR) {
838
+ pool.markFailure(k.id, '额度不足', `剩余 ${row.state.available} 魔粒`, nextQuotaResetAt())
839
+ continue
840
+ }
841
+ pool.restore(k.id)
781
842
  }
782
- pool.restore(k.id)
783
- }
784
- refreshPanel()
785
- refreshStrip()
786
- const ok = rows.filter((r) => r.state?.ok && r.state.available >= QUOTA_FLOOR).length
787
- const invalid = rows.filter((r) => !r.state?.ok).length
788
- const short = rows.length - ok - invalid
789
- const parts = [t('partOk', { ok, total: keys.length })]
790
- if (short > 0) parts.push(t('partShort', { n: short }))
791
- if (invalid > 0) parts.push(t('partInvalid', { n: invalid }))
792
- return { toast: t('toastTestDone', { parts: parts.join(' · ') }) }
843
+ const ok = rows.filter((r) => r.state?.ok && r.state.available >= QUOTA_FLOOR).length
844
+ const invalid = rows.filter((r) => !r.state?.ok).length
845
+ const short = rows.length - ok - invalid
846
+ const parts = [t('partOk', { ok, total: keys.length })]
847
+ if (short > 0) parts.push(t('partShort', { n: short }))
848
+ if (invalid > 0) parts.push(t('partInvalid', { n: invalid }))
849
+ return t('toastTestDone', { parts: parts.join(' · ') })
793
850
  })
794
851
  },
795
852
  'restore-all': async (scope, event) => {
@@ -870,13 +927,13 @@ const modelRowOf = (entry, current, t) => {
870
927
  })
871
928
  return { toast: t('toastDeleted', { id }), refresh: true }
872
929
  },
873
- 'add-form': (scope, event) => {
930
+ 'add-form'(scope, event) {
874
931
  const t = makeT(event?.locale)
875
932
  const value = event?.value
876
933
  const raw = typeof value === 'string' ? value : value?.newKey ?? value?.value ?? ''
877
- return defer(t('pendingAdd'), async () => {
934
+ return runInBackground('add', t('pendingAdd'), async () => {
878
935
  const result = await addKey(raw, event?.locale)
879
- return { toast: result?.toast ?? t('toastNoNew') }
936
+ return result?.toast ?? t('toastNoNew')
880
937
  })
881
938
  },
882
939
  'key-back'() {
@@ -884,13 +941,12 @@ const modelRowOf = (entry, current, t) => {
884
941
  },
885
942
  'refresh-models'(scope, event) {
886
943
  const t = makeT(event?.locale)
887
- return defer(t('pendingModels'), async () => {
944
+ return runInBackground('models', t('pendingModels'), async () => {
888
945
  await refreshCatalog(true)
889
- return { toast: t('toastModelsReloaded') }
946
+ return t('toastModelsReloaded')
890
947
  })
891
948
  },
892
949
  }
893
-
894
950
  dshGo.panel('pool', { view: viewPool, data: panelData(buildPoolData), actions: panelActions })
895
951
  dshGo.panel('key', { view: viewKey, data: panelData(buildKeyData), actions: panelActions })
896
952
  dshGo.panel('models', { view: viewModels, data: panelData(buildModelsData), actions: panelActions })
@@ -932,11 +988,14 @@ const modelRowOf = (entry, current, t) => {
932
988
  },
933
989
  })
934
990
 
935
- dshGo.on('addkey', async (scope, event) => {
991
+ dshGo.on('addkey', (scope, event) => {
992
+ const t = makeT(event?.locale)
936
993
  const text = String(scope.clipboard ?? '').trim()
937
- if (!text) return { toast: makeT(event?.locale)('toastNoClipboard') }
938
- const result = await addKey(text, event?.locale)
939
- return { ...result, refresh: 'pool' }
994
+ if (!text) return { toast: t('toastNoClipboard') }
995
+ return runInBackground('add', t('pendingAdd'), async () => {
996
+ const result = await addKey(text, event?.locale)
997
+ return result?.toast ?? t('toastNoNew')
998
+ })
940
999
  })
941
1000
 
942
1001
  dshGo.on('open-pool', () => ({ open: 'panel:pool' }))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-modelscope-pool",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "description": "魔搭多 Key 聚合网关:无感路由(模型不变,Key 遇限流/额度不足/无权限/不存在自动换下一个);自动识别国际版/国内版令牌;单个 Key 可禁用/停用/恢复/删除/查看;全部密钥分页浏览;模型实际拉取国际版+国内版合并成简化名、按站点分组;额度低于 2 停用到次日 00:01、多 Key 轮换、限流临时失败冷却 5 分钟,并注册为 DSH 模型供应商",
5
5
  "license": "MIT",
6
6
  "author": "Yizutt",