dsh-modelscope-pool 0.10.3 → 0.11.1
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 +30 -3
- package/lib/catalog.js +4 -0
- package/lib/index.js +74 -44
- package/lib/provider.js +10 -3
- package/lib/specs.js +27 -0
- package/lib/web.js +71 -52
- package/package.json +2 -23
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
- [界面](#界面)
|
|
23
23
|
- [多语言](#多语言)
|
|
24
24
|
- [网页版与桌面版](#网页版与桌面版)
|
|
25
|
+
- [模型上下文与最大输出](#模型上下文与最大输出)
|
|
25
26
|
- [HTTP 接口](#http-接口)
|
|
26
27
|
- [接入其它客户端](#接入其它客户端)
|
|
27
28
|
- [常见问题](#常见问题)
|
|
@@ -121,7 +122,7 @@
|
|
|
121
122
|
### 方式一:从 npm 安装(推荐)
|
|
122
123
|
|
|
123
124
|
```bash
|
|
124
|
-
dsh plugin --profile web add dsh-modelscope-pool@0.
|
|
125
|
+
dsh plugin --profile web add dsh-modelscope-pool@0.11.0
|
|
125
126
|
```
|
|
126
127
|
|
|
127
128
|
一条命令搞定。装好后重启 App(或让插件重新加载),网关会在 `127.0.0.1:8899` 起来。
|
|
@@ -199,7 +200,7 @@ dsh plugin --profile web add /path/to/dsh-modelscope-pool
|
|
|
199
200
|
|
|
200
201
|
## 界面
|
|
201
202
|
|
|
202
|
-
插件在 dsh-go
|
|
203
|
+
插件在 dsh-go 的输入框上方放了一条常驻状态条,点它直接进「魔搭 Key 池」面板;另外注册了三个原生面板:
|
|
203
204
|
|
|
204
205
|
| 面板 | 位置 | 内容 |
|
|
205
206
|
|---|---|---|
|
|
@@ -207,7 +208,9 @@ dsh plugin --profile web add /path/to/dsh-modelscope-pool
|
|
|
207
208
|
| **Key 详情** | 侧栏 / 功能页 | 点列表任意一行进来:完整令牌、站点、额度明细、状态、失败原因,以及禁用 / 停用 / 恢复 / 删除(删除需连点两次确认) |
|
|
208
209
|
| **魔搭模型** | 对话标签 / 侧栏 / 功能页 | 按「国际版 / 国内版 / 双站」分组的模型列表,点一下设为默认,底部可重新拉取 |
|
|
209
210
|
|
|
210
|
-
|
|
211
|
+
输入框上方那条状态条显示「魔搭 93/95 · 2340 魔粒」,点一下就进面板——添加 Key、刷新额度都在面板里完成。
|
|
212
|
+
|
|
213
|
+
> 0.11.0 去掉了原先输入框上方的两个悬浮按钮:插件侧控制不了 App 给悬浮按钮加的投影,看起来像「胶囊按钮带阴影」。入口合并到状态条,一次点按到位,也不再有两层悬浮元素叠在一起。
|
|
211
214
|
|
|
212
215
|
---
|
|
213
216
|
|
|
@@ -319,6 +322,24 @@ dsh-go 里更进一步:这个插件会把自己注册成一个叫 **魔搭 Key
|
|
|
319
322
|
|
|
320
323
|
---
|
|
321
324
|
|
|
325
|
+
## 模型上下文与最大输出
|
|
326
|
+
|
|
327
|
+
魔搭的 `/v1/models` 只回 `id / owned_by / created`,详情接口和模型页也没有上下文字段——官方数字只写在**模型卡片的说明里**。所以插件对能确认的旗舰模型逐个填真实值,其余不声明、走插件默认(上下文 128K)。
|
|
328
|
+
|
|
329
|
+
| 模型 | 上下文 | 最大输出 | 卡片依据 |
|
|
330
|
+
|---|---|---|---|
|
|
331
|
+
| `DeepSeek-V4-Pro` | 1,000,000 | — | 参数表 Context Length = 1M |
|
|
332
|
+
| `DeepSeek-V4-Pro-0813` | 1,000,000 | 393,216 | 正文「最大输出长度 384K」 |
|
|
333
|
+
| `DeepSeek-V4.1-Flash` | 1,000,000 | — | 正文「up to one million tokens」 |
|
|
334
|
+
| `DeepSeek-V4.1-Flash-1017` | 1,000,000 | 262,144 | 参数表 `context_window` 1M / `max_tokens` ≥ 256K |
|
|
335
|
+
| `GLM-5.2` | 1,000,000 | — | 正文「Solid 1M Context」 |
|
|
336
|
+
|
|
337
|
+
「—」表示卡片没写,就不填:DSH 没拿到最大输出时用它自己的默认,比瞎编一个安全。
|
|
338
|
+
|
|
339
|
+
> 最大输出只在**明显小于上下文**时才填。`Step-5-Preview` 卡片写的是上下文 1M、最大输出也是 1M,两边相等会让 DSH 的「预留回复额度」把整个窗口吃掉,压缩预算变成 0,切模型反而报错,所以这个型号只按默认走。
|
|
340
|
+
|
|
341
|
+
新增模型:在 `lib/specs.js` 里加一行 `简化名 → { contextWindow, maxTokens }` 即可,键不区分大小写,没列到的自动回落到插件默认。
|
|
342
|
+
|
|
322
343
|
## 常见问题
|
|
323
344
|
|
|
324
345
|
**导入后一堆 Key 显示「令牌无效」?**
|
|
@@ -336,6 +357,12 @@ dsh-go 里更进一步:这个插件会把自己注册成一个叫 **魔搭 Key
|
|
|
336
357
|
**点「刷新额度」提示「魔搭 Key 池没有响应」?**
|
|
337
358
|
App 要求点按在 3 秒内返回,而刷新额度要联网逐个查所有 Key 的额度,早就超过 3 秒了。0.10.3 起这几个慢操作(刷新额度 / 测试全部 / 重新拉取模型 / 添加 Key)不再在点按里等:点一下立刻在**面板顶部**显示「正在刷新额度」,**3 秒后自动收起**(操作不会中断),跑完再把结果挂上去 3 秒。如果你还在 0.10.0 或更早,升级即可。
|
|
338
359
|
|
|
360
|
+
**一次粘贴很多个 Key,只加进去一部分?**
|
|
361
|
+
每行一个,或者用逗号、空格、分号分隔都行,重复的会自动跳过。0.11.0 起入库是本地写文件、立刻返回,认站点和查额度放到后台,粘 50 个也不会卡住,也不会挡住下一批。
|
|
362
|
+
|
|
363
|
+
**为什么模型列表里没有国内版模型?**
|
|
364
|
+
面板和模型页只显示「池里真有这个站点 Key」的模型。只有国际版 Key 时,国内版独占的模型不会出现;等加进国内版 Key 并查过额度后,它们会自动出现。
|
|
365
|
+
|
|
339
366
|
**端口被占用?**
|
|
340
367
|
改 `config.json` 的 `port`,同时把客户端和供应商配置里的地址一起改掉。
|
|
341
368
|
|
package/lib/catalog.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// 各有一份模型表,两边取并集并按「简化名」合并:同一个模型在两个站点的 id 可能不同
|
|
3
3
|
// (例如 zai-org/GLM-4.7-Flash 与 ZhipuAI/GLM-4.7-Flash),合并后只出现一行,
|
|
4
4
|
// 真正请求时按 Key 所属站点换成那边的真实 id。
|
|
5
|
+
import { specFor } from './specs.js'
|
|
6
|
+
|
|
5
7
|
const REGION_LABEL = { ai: '国际版', cn: '国内版' }
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -74,6 +76,8 @@ export function createCatalog({ regions, preferred = ['ai', 'cn'], ttlMs = 300_0
|
|
|
74
76
|
regions: preferred.filter((key) => Boolean(row.variants[key])),
|
|
75
77
|
variants: row.variants,
|
|
76
78
|
rawIds: row.rawIds,
|
|
79
|
+
// 官方规格只对列在 specs.js 里的旗舰模型有值,其余为空。
|
|
80
|
+
...specFor(row.name),
|
|
77
81
|
}))
|
|
78
82
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
79
83
|
cache = { at: Date.now(), entries }
|
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 = (
|
|
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
|
|
703
|
-
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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: () =>
|
|
1069
|
-
getEntries: () =>
|
|
1098
|
+
getModels: () => visibleModels().map((entry) => entry.id),
|
|
1099
|
+
getEntries: () => visibleModels(),
|
|
1070
1100
|
logger,
|
|
1071
1101
|
})
|
|
1072
1102
|
providerState = {
|
package/lib/provider.js
CHANGED
|
@@ -148,8 +148,9 @@ export async function registerPoolProvider(ctx, { providerId, displayName, getPo
|
|
|
148
148
|
provider: providerId,
|
|
149
149
|
baseUrl: baseUrl(),
|
|
150
150
|
input: ['text'],
|
|
151
|
-
// DSH
|
|
152
|
-
contextWindow: DEFAULT_CONTEXT_WINDOW,
|
|
151
|
+
// 有官方规格的用官方值;其余给保守默认,DSH 拒绝没有正整数上下文的模型。
|
|
152
|
+
contextWindow: Number(entry.contextWindow) > 0 ? Number(entry.contextWindow) : DEFAULT_CONTEXT_WINDOW,
|
|
153
|
+
...(Number(entry.maxTokens) > 0 ? { maxTokens: Number(entry.maxTokens) } : {}),
|
|
153
154
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
154
155
|
reasoning: false,
|
|
155
156
|
}))
|
|
@@ -216,7 +217,13 @@ export async function registerPoolProvider(ctx, { providerId, displayName, getPo
|
|
|
216
217
|
let off
|
|
217
218
|
try {
|
|
218
219
|
off = ctx.llm.registerModelDiscovery(entryId, async () =>
|
|
219
|
-
buildModels().map((model) => ({
|
|
220
|
+
buildModels().map((model) => ({
|
|
221
|
+
id: model.id,
|
|
222
|
+
name: model.name,
|
|
223
|
+
contextWindow: model.contextWindow,
|
|
224
|
+
...(model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens }),
|
|
225
|
+
inputModalities: ['text'],
|
|
226
|
+
})),
|
|
220
227
|
)
|
|
221
228
|
} catch (error) {
|
|
222
229
|
logger?.warn?.(`modelscope-pool: 模型发现注册失败(${String(error?.message ?? error)})`)
|
package/lib/specs.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// 官方规格:只收录能在模型卡片里找到明确写法的模型,其余不声明,走插件默认。
|
|
2
|
+
// 数据来源是 ModelScope 模型卡片(README.md),不是接口字段 —— 魔搭的 /v1/models
|
|
3
|
+
// 只回 id / owned_by / created,详情接口也没有上下文字段,所以只能按卡片逐个确认。
|
|
4
|
+
//
|
|
5
|
+
// 字段含义(DSH 模型发现契约):
|
|
6
|
+
// contextWindow 请求 + 回复合计的最大上下文
|
|
7
|
+
// maxTokens 最大输出 token 数(仅当卡片明确给出,且明显小于上下文时才填;
|
|
8
|
+
// 两者相等时填了会让 DSH 的压缩预算归零,反而切不过去)
|
|
9
|
+
export const OFFICIAL_SPECS = {
|
|
10
|
+
// 卡片表格:Context Length = 1M(正文:both supporting a context length of one million tokens)
|
|
11
|
+
'deepseek-v4-pro': { contextWindow: 1_000_000 },
|
|
12
|
+
// 卡片正文:recommend a maximum output length of 384K tokens
|
|
13
|
+
'deepseek-v4-pro-0813': { contextWindow: 1_000_000, maxTokens: 384 * 1024 },
|
|
14
|
+
// 卡片正文:support for contexts of up to one million tokens
|
|
15
|
+
'deepseek-v4.1-flash': { contextWindow: 1_000_000 },
|
|
16
|
+
// 卡片参数表:context_window = 1M tokens,max_tokens ≥ 256K
|
|
17
|
+
'deepseek-v4.1-flash-1017': { contextWindow: 1_000_000, maxTokens: 256 * 1024 },
|
|
18
|
+
// 卡片正文:Solid 1M Context
|
|
19
|
+
'glm-5.2': { contextWindow: 1_000_000 },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 按简化名查官方规格(大小写不敏感),查不到返回空对象。 */
|
|
23
|
+
export function specFor(name) {
|
|
24
|
+
const key = String(name ?? '').trim().toLowerCase()
|
|
25
|
+
if (!key) return {}
|
|
26
|
+
return OFFICIAL_SPECS[key] ?? {}
|
|
27
|
+
}
|
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:#
|
|
16
|
-
'@media (prefers-color-scheme:dark){:root{--bg:#
|
|
17
|
-
'*{box-sizing:border-box}',
|
|
18
|
-
'body{margin:0;
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'.
|
|
23
|
-
'.
|
|
24
|
-
'.
|
|
25
|
-
'.
|
|
26
|
-
'.
|
|
27
|
-
'.
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'.
|
|
35
|
-
'.
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
-
'
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
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="
|
|
62
|
-
'<
|
|
63
|
-
'<
|
|
64
|
-
'<div class="
|
|
65
|
-
'
|
|
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-
|
|
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.
|
|
4
|
-
"description": "魔搭多 Key
|
|
3
|
+
"version": "0.11.1",
|
|
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",
|