dsh-modelscope-pool 0.10.3 → 0.11.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/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.3
124
+ dsh plugin --profile web add dsh-modelscope-pool@0.11.0
125
125
  ```
126
126
 
127
127
  一条命令搞定。装好后重启 App(或让插件重新加载),网关会在 `127.0.0.1:8899` 起来。
@@ -199,7 +199,7 @@ dsh plugin --profile web add /path/to/dsh-modelscope-pool
199
199
 
200
200
  ## 界面
201
201
 
202
- 插件在 dsh-go 的对话界面加了入口(输入框上方两个按钮 + 一条常驻状态条),另外注册了三个原生面板:
202
+ 插件在 dsh-go 的输入框上方放了一条常驻状态条,点它直接进「魔搭 Key 池」面板;另外注册了三个原生面板:
203
203
 
204
204
  | 面板 | 位置 | 内容 |
205
205
  |---|---|---|
@@ -207,7 +207,9 @@ dsh plugin --profile web add /path/to/dsh-modelscope-pool
207
207
  | **Key 详情** | 侧栏 / 功能页 | 点列表任意一行进来:完整令牌、站点、额度明细、状态、失败原因,以及禁用 / 停用 / 恢复 / 删除(删除需连点两次确认) |
208
208
  | **魔搭模型** | 对话标签 / 侧栏 / 功能页 | 按「国际版 / 国内版 / 双站」分组的模型列表,点一下设为默认,底部可重新拉取 |
209
209
 
210
- 输入框上方还有一条常驻状态条,显示池里可用 Key 的数量。
210
+ 输入框上方那条状态条显示「魔搭 93/95 · 2340 魔粒」,点一下就进面板——添加 Key、刷新额度都在面板里完成。
211
+
212
+ > 0.11.0 去掉了原先输入框上方的两个悬浮按钮:插件侧控制不了 App 给悬浮按钮加的投影,看起来像「胶囊按钮带阴影」。入口合并到状态条,一次点按到位,也不再有两层悬浮元素叠在一起。
211
213
 
212
214
  ---
213
215
 
@@ -336,6 +338,12 @@ dsh-go 里更进一步:这个插件会把自己注册成一个叫 **魔搭 Key
336
338
  **点「刷新额度」提示「魔搭 Key 池没有响应」?**
337
339
  App 要求点按在 3 秒内返回,而刷新额度要联网逐个查所有 Key 的额度,早就超过 3 秒了。0.10.3 起这几个慢操作(刷新额度 / 测试全部 / 重新拉取模型 / 添加 Key)不再在点按里等:点一下立刻在**面板顶部**显示「正在刷新额度」,**3 秒后自动收起**(操作不会中断),跑完再把结果挂上去 3 秒。如果你还在 0.10.0 或更早,升级即可。
338
340
 
341
+ **一次粘贴很多个 Key,只加进去一部分?**
342
+ 每行一个,或者用逗号、空格、分号分隔都行,重复的会自动跳过。0.11.0 起入库是本地写文件、立刻返回,认站点和查额度放到后台,粘 50 个也不会卡住,也不会挡住下一批。
343
+
344
+ **为什么模型列表里没有国内版模型?**
345
+ 面板和模型页只显示「池里真有这个站点 Key」的模型。只有国际版 Key 时,国内版独占的模型不会出现;等加进国内版 Key 并查过额度后,它们会自动出现。
346
+
339
347
  **端口被占用?**
340
348
  改 `config.json` 的 `port`,同时把客户端和供应商配置里的地址一起改掉。
341
349
 
package/lib/index.js CHANGED
@@ -330,6 +330,22 @@ const modelRowOf = (entry, current, t) => {
330
330
  }
331
331
  }
332
332
 
333
+ /**
334
+ * 面板和模型页里该显示哪些模型:只显示「池里真的有这个站点的 Key」的那些。
335
+ * 只有国际版 Key 时就不该冒出国内版模型,反之亦然。
336
+ * 站点一个都还没认出来时(刚导入、还没查过额度)先全部显示。
337
+ */
338
+ function visibleModels() {
339
+ const entries = (catalog?.entries ?? []).filter((entry) => isChatModel(entry.id))
340
+ const live = new Set((pool?.keys ?? []).map((k) => k.region).filter((r) => r === 'ai' || r === 'cn'))
341
+ if (live.size === 0) return entries
342
+ return entries.filter((entry) => {
343
+ const regions = entry.regions ?? []
344
+ if (regions.length === 0) return true
345
+ return regions.some((region) => live.has(region))
346
+ })
347
+ }
348
+
333
349
  function buildData(locale) {
334
350
  const t = makeT(locale)
335
351
  const s = pool ? pool.status() : { total: 0, available: 0, cooling: 0, keys: [], stats: { requests: 0 } }
@@ -338,7 +354,7 @@ const modelRowOf = (entry, current, t) => {
338
354
  const model = String(config.model ?? '')
339
355
  const modelEntry = catalog?.resolve(model)
340
356
  const currentModel = modelEntry?.id ?? model
341
- const models = (catalog?.entries ?? []).filter((entry) => isChatModel(entry.id))
357
+ const models = visibleModels()
342
358
  const routing = router.snapshot()
343
359
  const routed =
344
360
  routing.blockedPairs > 0 ? ` ${t('routed', { n: routing.blockedPairs })}` : ''
@@ -516,6 +532,7 @@ const modelRowOf = (entry, current, t) => {
516
532
  s.total === 0
517
533
  ? t('keyTitleEmpty')
518
534
  : t('keyTitlePage', { total: s.total, page: pageNo, pages: pageCount }),
535
+ newKey: '',
519
536
  keyPager:
520
537
  s.total > KEYS_PER_PAGE
521
538
  ? t('keyPager', { page: pageNo, pages: pageCount, size: KEYS_PER_PAGE })
@@ -660,6 +677,7 @@ const modelRowOf = (entry, current, t) => {
660
677
  keyTitle: d.keyTitle,
661
678
  keys: d.keys,
662
679
  focusTitle: d.focus.title,
680
+ newKey: '',
663
681
  }
664
682
  }
665
683
  const buildKeyData = (locale) => {
@@ -682,6 +700,20 @@ const modelRowOf = (entry, current, t) => {
682
700
  .filter((t) => /^ms-[A-Za-z0-9._-]{8,}$/.test(t))
683
701
  .filter((t, i, arr) => arr.indexOf(t) === i)
684
702
 
703
+ /** 表单提交回来的值形状不固定:字符串、{newKey}、{'data.newKey'} 都见过,统统认。 */
704
+ const pickText = (value) => {
705
+ if (typeof value === 'string') return value
706
+ if (!value || typeof value !== 'object') return ''
707
+ for (const key of ['newKey', 'data.newKey', 'value', 'text', 'input']) {
708
+ if (typeof value[key] === 'string' && value[key].trim()) return value[key]
709
+ }
710
+ for (const item of Object.values(value)) {
711
+ if (typeof item === 'string' && item.trim()) return item
712
+ }
713
+ return ''
714
+ }
715
+
716
+ /** 从泡里抠令牌、批量入库。入库是本地写文件,很快;认站点和查额度放后台。 */
685
717
  const addKey = async (raw, locale) => {
686
718
  const t = makeT(locale)
687
719
  const text = String(raw ?? '').trim()
@@ -698,33 +730,43 @@ const modelRowOf = (entry, current, t) => {
698
730
  }
699
731
  if (added.length === 0) return { toast: skipped[0] ?? t('toastNoNew') }
700
732
 
701
- // 加进来就逐个问额度接口:既能认出站点,也能当场发现令牌无效或额度不够。
702
- const regions = { ai: 0, cn: 0 }
703
- let invalid = 0
704
- let low = 0
705
- for (const { id, token } of added) {
706
- const state = await quota.refreshOne(id, token, { force: true })
707
- if (!state?.ok) {
708
- invalid++
709
- pool.markFailure(id, '\u4ee4\u724c\u65e0\u6548', String(state?.error ?? '').slice(0, 80))
710
- continue
711
- }
712
- if (state.region) {
713
- regions[state.region]++
714
- await pool.setRegion(id, state.region)
715
- }
716
- applyQuota(id, state)
717
- if (state.available < QUOTA_FLOOR) low++
718
- }
733
+ // 先给用户一个立刻的答复,几十个 Key 也不用等。
734
+ const bits = [t('addDone', { n: added.length })]
735
+ if (skipped.length > 0) bits.push(t('addSkipped', { n: skipped.length }))
719
736
  refreshPanel()
720
737
  refreshStrip()
721
738
 
722
- const bits = [t('addDone', { n: added.length })]
723
- if (regions.ai > 0) bits.push(t('addIntl', { n: regions.ai }))
724
- if (regions.cn > 0) bits.push(t('addCn', { n: regions.cn }))
725
- if (low > 0) bits.push(t('addLow', { n: low }))
726
- if (invalid > 0) bits.push(t('addInvalid', { n: invalid }))
727
- if (skipped.length > 0) bits.push(t('addSkipped', { n: skipped.length }))
739
+ // 后台逐个认站点 / 查额度:失败的不阻塞入库,也不影响下一批。
740
+ void (async () => {
741
+ let invalid = 0
742
+ let low = 0
743
+ const regions = { ai: 0, cn: 0 }
744
+ for (const { id, token } of added) {
745
+ try {
746
+ const state = await quota.refreshOne(id, token, { force: true })
747
+ if (!state?.ok) {
748
+ invalid++
749
+ pool.markFailure(id, '\u4ee4\u724c\u65e0\u6548', String(state?.error ?? '').slice(0, 80))
750
+ continue
751
+ }
752
+ if (state.region) {
753
+ regions[state.region]++
754
+ await pool.setRegion(id, state.region)
755
+ }
756
+ applyQuota(id, state)
757
+ if (state.available < QUOTA_FLOOR) low++
758
+ } catch {}
759
+ }
760
+ refreshPanel()
761
+ refreshStrip()
762
+ const tail = [t('addDone', { n: added.length })]
763
+ if (regions.ai > 0) tail.push(t('addIntl', { n: regions.ai }))
764
+ if (regions.cn > 0) tail.push(t('addCn', { n: regions.cn }))
765
+ if (low > 0) tail.push(t('addLow', { n: low }))
766
+ if (invalid > 0) tail.push(t('addInvalid', { n: invalid }))
767
+ setNotice(tail.join(t('joinArrow')))
768
+ })()
769
+
728
770
  return { toast: bits.join(t('joinArrow')) }
729
771
  }
730
772
 
@@ -927,14 +969,12 @@ const modelRowOf = (entry, current, t) => {
927
969
  })
928
970
  return { toast: t('toastDeleted', { id }), refresh: true }
929
971
  },
930
- 'add-form'(scope, event) {
972
+ async 'add-form'(scope, event) {
931
973
  const t = makeT(event?.locale)
932
- const value = event?.value
933
- const raw = typeof value === 'string' ? value : value?.newKey ?? value?.value ?? ''
934
- return runInBackground('add', t('pendingAdd'), async () => {
935
- const result = await addKey(raw, event?.locale)
936
- return result?.toast ?? t('toastNoNew')
937
- })
974
+ const raw = pickText(event?.value)
975
+ if (!raw) return { toast: t('toastNoInput'), refresh: true }
976
+ const result = await addKey(raw, event?.locale)
977
+ return { toast: result?.toast ?? t('toastNoNew'), refresh: true }
938
978
  },
939
979
  'key-back'() {
940
980
  return { open: 'panel:pool' }
@@ -988,16 +1028,6 @@ const modelRowOf = (entry, current, t) => {
988
1028
  },
989
1029
  })
990
1030
 
991
- dshGo.on('addkey', (scope, event) => {
992
- const t = makeT(event?.locale)
993
- const text = String(scope.clipboard ?? '').trim()
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
- })
999
- })
1000
-
1001
1031
  dshGo.on('open-pool', () => ({ open: 'panel:pool' }))
1002
1032
  })
1003
1033
 
@@ -1065,8 +1095,8 @@ const modelRowOf = (entry, current, t) => {
1065
1095
  entryId,
1066
1096
  getPort: () => proxy?.port ?? 0,
1067
1097
  getKey: () => config.apiKey,
1068
- getModels: () => catalog?.ids ?? [],
1069
- getEntries: () => catalog?.entries ?? [],
1098
+ getModels: () => visibleModels().map((entry) => entry.id),
1099
+ getEntries: () => visibleModels(),
1070
1100
  logger,
1071
1101
  })
1072
1102
  providerState = {
package/lib/web.js CHANGED
@@ -9,64 +9,79 @@ const HTML = [
9
9
  '<!doctype html>',
10
10
  '<html lang="zh-CN"><head>',
11
11
  '<meta charset="utf-8">',
12
- '<meta name="viewport" content="width=device-width,initial-scale=1">',
12
+ '<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">',
13
13
  '<title>ModelScope key pool</title>',
14
14
  '<style>',
15
- ':root{color-scheme:light dark;--bg:#fff;--fg:#16181d;--muted:#6b7078;--line:#e3e6ea;--card:#f7f8fa;--good:#1f7a4d;--warn:#8a6100;--bad:#b3261e;--btn:#eceff3}',
16
- '@media (prefers-color-scheme:dark){:root{--bg:#16181d;--fg:#e8eaee;--muted:#9aa0aa;--line:#2b2f36;--card:#1d2026;--good:#5fd39b;--warn:#e0b355;--bad:#ff8a80;--btn:#262b33}}',
17
- '*{box-sizing:border-box}',
18
- 'body{margin:0;padding:16px 14px 40px;background:var(--bg);color:var(--fg);font:14px/1.5 -apple-system,system-ui,"Segoe UI",Roboto,"Noto Sans SC",sans-serif}',
19
- 'h1{margin:0 0 2px;font-size:18px}',
20
- '.sub{color:var(--muted);font-size:12px;margin-bottom:14px}',
21
- '.stats{display:flex;gap:10px;margin-bottom:12px}',
22
- '.stat{flex:1;background:var(--card);border:1px solid var(--line);border-radius:10px;padding:10px}',
23
- '.stat b{display:block;font-size:20px;line-height:1.2}',
24
- '.stat span{color:var(--muted);font-size:12px}',
25
- '.bar{height:8px;border-radius:6px;background:var(--line);overflow:hidden;margin:6px 0 4px}',
26
- '.bar i{display:block;height:100%;background:var(--good)}',
27
- '.row{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0}',
28
- 'button{background:var(--btn);color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:7px 12px;font:inherit;cursor:pointer}',
29
- 'button:active{opacity:.7}',
30
- 'button.pri{background:var(--fg);color:var(--bg);border-color:var(--fg)}',
31
- 'table{width:100%;border-collapse:collapse;font-size:13px}',
32
- 'th,td{text-align:left;padding:7px 6px;border-bottom:1px solid var(--line);vertical-align:middle}',
33
- 'th{color:var(--muted);font-weight:500;font-size:12px}',
34
- '.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}',
35
- '.ok{color:var(--good)}.warn{color:var(--warn)}.bad{color:var(--bad)}',
36
- '.muted{color:var(--muted)}',
37
- 'td.ops{white-space:nowrap;text-align:right}',
38
- 'td.ops button{padding:4px 8px;font-size:12px;margin-left:4px}',
39
- 'textarea,select{width:100%;background:var(--card);color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:8px;font:inherit}',
40
- 'textarea{min-height:64px;resize:vertical}',
41
- '.toast{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);background:var(--fg);color:var(--bg);padding:9px 16px;border-radius:20px;font-size:13px;opacity:0;transition:opacity .2s;pointer-events:none;max-width:88vw}',
42
- '.toast.on{opacity:.95}',
43
- 'details{margin-top:18px}summary{cursor:pointer;color:var(--muted);font-size:13px}',
44
- '</style></head><body>',
45
- '<h1 id="title">魔搭 Key 池</h1>',
46
- '<div class="sub" id="sub"></div>',
47
- '<div class="stats">',
48
- '<div class="stat"><b id="s-ok">–</b><span id="l-ok"></span></div>',
49
- '<div class="stat"><b id="s-cool">–</b><span id="l-cool"></span></div>',
50
- '<div class="stat"><b id="s-magic">–</b><span id="l-magic"></span></div>',
51
- '</div>',
52
- '<div class="bar"><i id="bar" style="width:0"></i></div>',
53
- '<div class="sub" id="magic-cap"></div>',
54
- '<div class="row">',
15
+ ':root{color-scheme:light dark;--bg:#f6f7f9;--card:#fff;--fg:#14171d;--muted:#6b7280;--line:#e5e7eb;--accent:#0f766e;--accent-fg:#fff;--good:#15803d;--warn:#a16207;--bad:#b91c1c;--chip:#eef1f5}',
16
+ '@media (prefers-color-scheme:dark){:root{--bg:#14161a;--card:#1c1f25;--fg:#e9ebef;--muted:#9aa1ad;--line:#2b3038;--accent:#2dd4bf;--accent-fg:#0b0d10;--good:#4ade80;--warn:#fbbf24;--bad:#f87171;--chip:#242932}}',
17
+ '*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}',
18
+ 'body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.55 -apple-system,system-ui,"Segoe UI",Roboto,"Noto Sans SC",sans-serif;padding:0 14px 40px}',
19
+ '.wrap{max-width:760px;margin:0 auto}',
20
+ 'header{position:sticky;top:0;z-index:5;display:flex;align-items:center;gap:10px;padding:14px 0 10px;background:linear-gradient(var(--bg) 78%,transparent)}',
21
+ 'header h1{margin:0;font-size:19px;font-weight:650;letter-spacing:.2px}',
22
+ 'header .sub{margin:2px 0 0;color:var(--muted);font-size:12px}',
23
+ 'header .grow{flex:1;min-width:0}',
24
+ '.ghost{background:var(--chip);border:1px solid var(--line);color:var(--fg);border-radius:999px;padding:6px 12px;font:inherit;font-size:13px}',
25
+ '.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin:6px 0 14px}',
26
+ '.card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:12px 13px}',
27
+ '.card b{display:block;font-size:24px;line-height:1.15;font-weight:680;font-variant-numeric:tabular-nums}',
28
+ '.card span{display:block;margin-top:2px;color:var(--muted);font-size:12px}',
29
+ '.card.ok b{color:var(--good)}.card.warn b{color:var(--warn)}.card.info b{color:var(--accent)}',
30
+ '.magic{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:12px 13px;margin-bottom:14px}',
31
+ '.magic .row{display:flex;justify-content:space-between;gap:10px;font-size:13px;color:var(--muted)}',
32
+ '.magic .row b{color:var(--fg);font-weight:600;font-size:13px}',
33
+ '.bar{height:9px;border-radius:99px;background:var(--chip);overflow:hidden;margin-top:9px}',
34
+ '.bar i{display:block;height:100%;border-radius:99px;background:linear-gradient(90deg,var(--accent),color-mix(in srgb,var(--accent) 55%,#fff));transition:width .35s ease}',
35
+ '.toolbar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px}',
36
+ 'button{font:inherit;border-radius:11px;border:1px solid var(--line);background:var(--card);color:var(--fg);padding:9px 14px;cursor:pointer}',
37
+ 'button:active{transform:scale(.985)}',
38
+ 'button.pri{background:var(--accent);border-color:var(--accent);color:var(--accent-fg);font-weight:600}',
39
+ '.tablewrap{background:var(--card);border:1px solid var(--line);border-radius:14px;overflow:hidden}',
40
+ 'table{width:100%;border-collapse:collapse;font-size:13.5px}',
41
+ 'thead th{position:sticky;top:0;background:var(--card);color:var(--muted);font-weight:500;font-size:12px;text-align:left;padding:10px 12px;border-bottom:1px solid var(--line);white-space:nowrap}',
42
+ 'tbody td{padding:10px 12px;border-bottom:1px solid var(--line);vertical-align:middle}',
43
+ 'tbody tr:last-child td{border-bottom:0}',
44
+ 'tbody tr:nth-child(even){background:color-mix(in srgb,var(--chip) 45%,transparent)}',
45
+ '.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px}',
46
+ '.ok{color:var(--good)}.warn{color:var(--warn)}.bad{color:var(--bad)}.muted{color:var(--muted)}',
47
+ 'td.ops{text-align:right;white-space:nowrap}',
48
+ 'td.ops button{padding:5px 9px;font-size:12px;border-radius:8px;margin-left:5px;background:transparent}',
49
+ 'td.ops button:hover{background:var(--chip)}',
50
+ 'td.ops button.danger{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 35%,var(--line))}',
51
+ '.pager{display:flex;align-items:center;justify-content:center;gap:12px;margin:12px 0 18px;color:var(--muted);font-size:13px}',
52
+ '.pager button{padding:7px 14px}',
53
+ '.panel{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:13px;margin-bottom:14px}',
54
+ '.panel h2{margin:0 0 9px;font-size:14px;font-weight:600}',
55
+ 'textarea,select{width:100%;background:var(--bg);color:var(--fg);border:1px solid var(--line);border-radius:11px;padding:10px 12px;font:inherit;font-size:14px}',
56
+ 'textarea{min-height:86px;resize:vertical;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px}',
57
+ '.panel .foot{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top:9px}',
58
+ '.panel .foot span{color:var(--muted);font-size:12px}',
59
+ '.toast{position:fixed;left:50%;bottom:26px;transform:translateX(-50%) translateY(8px);background:var(--fg);color:var(--bg);padding:10px 18px;border-radius:999px;font-size:13px;opacity:0;transition:opacity .18s,transform .18s;pointer-events:none;max-width:90vw;z-index:20}',
60
+ '.toast.on{opacity:.96;transform:translateX(-50%) translateY(0)}',
61
+ '@media (max-width:430px){.cards{grid-template-columns:repeat(3,1fr);gap:8px}.card{padding:10px}.card b{font-size:20px}td.ops button{padding:5px 7px;margin-left:4px}}',
62
+ '</style></head><body><div class="wrap">',
63
+ '<header><div class="grow"><h1 id="title">魔搭 Key 池</h1><p class="sub" id="sub"></p></div>',
64
+ '<button class="ghost" id="b-lang">English</button></header>',
65
+ '<section class="cards">',
66
+ '<div class="card ok"><b id="s-ok">–</b><span id="l-ok"></span></div>',
67
+ '<div class="card warn"><b id="s-cool">–</b><span id="l-cool"></span></div>',
68
+ '<div class="card info"><b id="s-magic">–</b><span id="l-magic"></span></div>',
69
+ '</section>',
70
+ '<section class="magic"><div class="row"><b id="magic-label"></b><span id="magic-cap"></span></div>',
71
+ '<div class="bar"><i id="bar" style="width:0"></i></div></section>',
72
+ '<div class="toolbar">',
55
73
  '<button class="pri" data-act="refresh" id="b-refresh"></button>',
56
74
  '<button data-act="test" id="b-test"></button>',
57
75
  '<button data-act="restoreAll" id="b-restore"></button>',
58
- '<button id="b-lang"></button>',
59
76
  '</div>',
60
- '<table><thead><tr id="head"></tr></thead><tbody id="rows"></tbody></table>',
61
- '<div class="row"><button id="b-prev"></button><button id="b-next"></button></div>',
62
- '<details><summary id="add-sum"></summary>',
63
- '<div class="row"><textarea id="add-text" placeholder="ms-..."></textarea></div>',
64
- '<div class="row"><button class="pri" id="b-add"></button></div>',
65
- '</details>',
66
- '<details><summary id="model-sum"></summary>',
67
- '<div class="row"><select id="model-sel"></select></div>',
68
- '</details>',
77
+ '<div class="tablewrap"><table><thead><tr id="head"></tr></thead><tbody id="rows"></tbody></table></div>',
78
+ '<div class="pager"><button id="b-prev"></button><span id="page-info"></span><button id="b-next"></button></div>',
79
+ '<section class="panel"><h2 id="add-sum"></h2>',
80
+ '<textarea id="add-text" placeholder="ms-..."></textarea>',
81
+ '<div class="foot"><span id="add-hint"></span><button class="pri" id="b-add"></button></div></section>',
82
+ '<section class="panel"><h2 id="model-sum"></h2><select id="model-sel"></select></section>',
69
83
  '<div class="toast" id="toast"></div>',
84
+ '</div>',
70
85
  '<script>',
71
86
  'var L=null,S=null,PAGE=0,PER=20,ARM="";',
72
87
  'var DICT={',
@@ -109,13 +124,17 @@ const HTML = [
109
124
  ' document.getElementById("l-magic").textContent=L.magic;',
110
125
  ' var pct=S.magic.total>0?Math.max(0,Math.min(100,S.magic.left/S.magic.total*100)):0;',
111
126
  ' document.getElementById("bar").style.width=pct.toFixed(1)+"%";',
112
- ' document.getElementById("magic-cap").textContent=" "+L.left+" "+S.magic.left+" / "+L.total+" "+S.magic.total;',
127
+ ' document.getElementById("magic-label").textContent=L.usage;',
128
+ ' document.getElementById("magic-cap").textContent=L.left+" "+S.magic.left+" / "+L.total+" "+S.magic.total;',
129
+ ' document.getElementById("add-hint").textContent=L.addHint;',
113
130
  ' document.getElementById("b-refresh").textContent=L.refresh;',
114
131
  ' document.getElementById("b-test").textContent=L.test;',
115
132
  ' document.getElementById("b-restore").textContent=L.restoreAll;',
116
133
  ' document.getElementById("b-prev").textContent=L.prev;',
117
134
  ' document.getElementById("b-next").textContent=L.next;',
118
135
  ' document.getElementById("b-add").textContent=L.add;',
136
+ ' var pg=document.getElementById("page-info");',
137
+ ' if(pg){var total=Math.max(1,Math.ceil(S.keys.length/PER));pg.textContent=L.page.replace("{p}",String(PAGE+1)).replace("{n}",String(total))}',
119
138
  ' document.getElementById("add-sum").textContent=L.addSum;',
120
139
  ' document.getElementById("model-sum").textContent=L.modelSum;',
121
140
  ' var head=document.getElementById("head");head.innerHTML="";',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-modelscope-pool",
3
- "version": "0.10.3",
4
- "description": "魔搭多 Key 聚合网关:无感路由(模型不变,Key 遇限流/额度不足/无权限/不存在自动换下一个);自动识别国际版/国内版令牌;单个 Key 可禁用/停用/恢复/删除/查看;全部密钥分页浏览;模型实际拉取国际版+国内版合并成简化名、按站点分组;额度低于 2 停用到次日 00:01、多 Key 轮换、限流临时失败冷却 5 分钟,并注册为 DSH 模型供应商",
3
+ "version": "0.11.0",
4
+ "description": "魔搭多 Key 聚合网关 + DSH 原生插件:模型不变,Key 遇限流、额度不足、无权限或令牌失效自动无感换下一个;自动识别国际版/国内版令牌;Key 池管理、魔粒用量、多语言界面;也可在网页版/桌面版管理。",
5
5
  "license": "MIT",
6
6
  "author": "Yizutt",
7
7
  "repository": {
@@ -39,28 +39,7 @@
39
39
  "en": "ModelScope keys"
40
40
  },
41
41
  "icon": "cloud",
42
- "wants": [
43
- "clipboard"
44
- ],
45
42
  "contributes": {
46
- "composer.actions": [
47
- {
48
- "id": "addkey",
49
- "label": {
50
- "zh": "添加 Key",
51
- "en": "Add key"
52
- },
53
- "icon": "paste"
54
- },
55
- {
56
- "id": "open-pool",
57
- "label": {
58
- "zh": "Key 池",
59
- "en": "Key pool"
60
- },
61
- "icon": "cloud"
62
- }
63
- ],
64
43
  "composer.strip": [
65
44
  {
66
45
  "id": "pool-strip",