dsh-modelscope-pool 0.10.0 → 0.10.2
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 +35 -7
- package/lib/i18n.js +6 -0
- package/lib/index.js +90 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# dsh-modelscope-pool
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-modelscope-pool)
|
|
3
4
|
[](LICENSE)
|
|
4
5
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
5
6
|
[](https://nodejs.org/)
|
|
@@ -117,29 +118,53 @@
|
|
|
117
118
|
- Node.js ≥ 20
|
|
118
119
|
- 一个魔搭账号和它的 API Key(`ms-` 开头),在 <https://modelscope.cn/my/myaccesstoken> 或 <https://modelscope.ai> 生成
|
|
119
120
|
|
|
120
|
-
###
|
|
121
|
+
### 方式一:从 npm 安装(推荐)
|
|
121
122
|
|
|
122
123
|
```bash
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
dsh plugin --profile web add dsh-modelscope-pool@0.10.0
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
一条命令搞定。装好后重启 App(或让插件重新加载),网关会在 `127.0.0.1:8899` 起来。
|
|
128
|
+
|
|
129
|
+
升级就是换版本号重装:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
dsh plugin --profile web add dsh-modelscope-pool@<新版本>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
> 建议写死精确版本。npm 上其他包常见的 `latest`、`^` 范围在这里没有好处——插件会监听端口、读写本机文件,版本变化值得你自己看一眼。
|
|
136
|
+
|
|
137
|
+
只想要包本身、不需要 dsh 帮你装:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npm i dsh-modelscope-pool
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
> 每个版本的改动见 [Releases](https://github.com/Yizutt/dsh-modelscope-pool/releases)。
|
|
144
|
+
|
|
145
|
+
### 方式二:从源码安装(要改代码时用)
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# 目录名要和 package.json 的 name 一致
|
|
125
149
|
mkdir -p "$DSH_HOME/dsh-go/plugins"
|
|
126
150
|
cd "$DSH_HOME/dsh-go/plugins"
|
|
127
151
|
git clone https://github.com/Yizutt/dsh-modelscope-pool.git dsh-modelscope-pool
|
|
128
152
|
|
|
129
|
-
# 2. 装进当前 profile
|
|
130
153
|
dsh plugin --profile web add "$DSH_HOME/dsh-go/plugins/dsh-modelscope-pool"
|
|
131
154
|
```
|
|
132
155
|
|
|
133
|
-
|
|
156
|
+
装的是指向这个目录的链接,所以**目录不能删**。改完 `lib/` 里的代码需要在 App 的「引擎」页重启引擎才生效。
|
|
134
157
|
|
|
135
|
-
###
|
|
158
|
+
### 方式三:只挂到 Harness(网页版 / 桌面版)
|
|
136
159
|
|
|
137
|
-
插件本身就是一个 Cordis bundle(`package.json` 里的 `dsh.bundle.patch` 指向 `cordis.patch.yml
|
|
160
|
+
插件本身就是一个 Cordis bundle(`package.json` 里的 `dsh.bundle.patch` 指向 `cordis.patch.yml`),所以不装 dsh-go 也能用:
|
|
138
161
|
|
|
139
162
|
```bash
|
|
140
163
|
dsh plugin --profile web add /path/to/dsh-modelscope-pool
|
|
141
164
|
```
|
|
142
165
|
|
|
166
|
+
网页版的管理页在 `http://127.0.0.1:3080/modelscope-pool`,见[网页版与桌面版](#网页版与桌面版)。
|
|
167
|
+
|
|
143
168
|
---
|
|
144
169
|
|
|
145
170
|
## 配置
|
|
@@ -308,6 +333,9 @@ dsh-go 里更进一步:这个插件会把自己注册成一个叫 **魔搭 Key
|
|
|
308
333
|
**Key 一直不自动恢复?**
|
|
309
334
|
看看是不是被「手动禁用」了(`manual` 标记)。手动禁用不会被自动恢复流程放开,需要点「恢复」。
|
|
310
335
|
|
|
336
|
+
**点「刷新额度」提示「魔搭 Key 池没有响应」?**
|
|
337
|
+
App 要求点按在 3 秒内返回,而刷新额度要联网逐个查所有 Key 的额度,早就超过 3 秒了。0.10.2 起这几个慢操作(刷新额度 / 测试全部 / 重新拉取模型 / 添加 Key)不再在点按里等:点一下立刻在**面板顶部**显示「正在刷新额度」,**3 秒后自动收起**(操作不会中断),跑完再把结果挂上去 3 秒。如果你还在 0.10.0 或更早,升级即可。
|
|
338
|
+
|
|
311
339
|
**端口被占用?**
|
|
312
340
|
改 `config.json` 的 `port`,同时把客户端和供应商配置里的地址一起改掉。
|
|
313
341
|
|
package/lib/i18n.js
CHANGED
|
@@ -129,6 +129,12 @@ const S = {
|
|
|
129
129
|
trailingCurrent: ['当前', 'Current'],
|
|
130
130
|
trailingUse: ['使用', 'Use'],
|
|
131
131
|
|
|
132
|
+
// 慢操作:点按后先回 pending,App 会显示等待
|
|
133
|
+
pendingRefresh: ['正在刷新额度', 'Refreshing quota'],
|
|
134
|
+
pendingTest: ['正在测试全部', 'Testing all'],
|
|
135
|
+
pendingModels: ['正在拉取模型', 'Loading models'],
|
|
136
|
+
pendingAdd: ['正在校验 Key', 'Checking keys'],
|
|
137
|
+
|
|
132
138
|
// 操作反馈
|
|
133
139
|
toastQuotaUpdated: ['额度已更新', 'Quota refreshed'],
|
|
134
140
|
toastModelSet: ['默认模型:{model}', 'Default model: {model}'],
|
package/lib/index.js
CHANGED
|
@@ -300,14 +300,16 @@ const modelRowOf = (entry, current, t) => {
|
|
|
300
300
|
const routing = router.snapshot()
|
|
301
301
|
const routed =
|
|
302
302
|
routing.blockedPairs > 0 ? ` ${t('routed', { n: routing.blockedPairs })}` : ''
|
|
303
|
+
const busy = noticeNow()
|
|
303
304
|
const hint =
|
|
304
|
-
|
|
305
|
+
busy ||
|
|
306
|
+
(s.total === 0
|
|
305
307
|
? t('hintNoKeys')
|
|
306
308
|
: s.cooling > 0
|
|
307
309
|
? `${t('hintCooling')}${routed}`
|
|
308
310
|
: model
|
|
309
311
|
? `${t('hintModel', { model })}${routed}`
|
|
310
|
-
: `${t('hintIdle')}${routed}`
|
|
312
|
+
: `${t('hintIdle')}${routed}`)
|
|
311
313
|
|
|
312
314
|
// 扇形图:Key 池的状态分布(可用 / 额度不足 / 令牌无效 / 其他冷却)
|
|
313
315
|
const order = [
|
|
@@ -515,6 +517,7 @@ const modelRowOf = (entry, current, t) => {
|
|
|
515
517
|
title: t('poolTitle'),
|
|
516
518
|
accent: 'teal',
|
|
517
519
|
children: [
|
|
520
|
+
{ type: 'callout', tone: 'info', text: { $: 'data.hint' } },
|
|
518
521
|
{
|
|
519
522
|
type: 'row',
|
|
520
523
|
children: [
|
|
@@ -531,7 +534,6 @@ const modelRowOf = (entry, current, t) => {
|
|
|
531
534
|
caption: { $: 'data.magic.brief' },
|
|
532
535
|
tone: 'good',
|
|
533
536
|
},
|
|
534
|
-
{ type: 'callout', tone: 'info', text: { $: 'data.hint' } },
|
|
535
537
|
{ type: 'text', text: { $: 'data.keyTitle' }, style: { size: 'title', weight: 'bold' } },
|
|
536
538
|
{ type: 'list', items: { $: 'data.keys' } },
|
|
537
539
|
{
|
|
@@ -728,11 +730,45 @@ const modelRowOf = (entry, current, t) => {
|
|
|
728
730
|
}
|
|
729
731
|
}
|
|
730
732
|
|
|
733
|
+
// 点按必须 3 秒内返回,否则 App 会提示「没有响应」。
|
|
734
|
+
// 联网的慢操作因此不在点按里等:先把一句话挂到面板顶部(data.hint),
|
|
735
|
+
// 3 秒后自动收起,真正的活儿在后台继续跑,跑完再挂一句结果,同样 3 秒收起。
|
|
736
|
+
let notice = ''
|
|
737
|
+
let noticeUntil = 0
|
|
738
|
+
const setNotice = (text, ms = 3000) => {
|
|
739
|
+
notice = String(text ?? '')
|
|
740
|
+
noticeUntil = notice ? Date.now() + ms : 0
|
|
741
|
+
refreshPanel()
|
|
742
|
+
refreshStrip()
|
|
743
|
+
}
|
|
744
|
+
const noticeNow = () => (noticeUntil > Date.now() ? notice : '')
|
|
745
|
+
const running = new Set()
|
|
746
|
+
const runInBackground = (key, label, work) => {
|
|
747
|
+
if (running.has(key)) return { toast: label }
|
|
748
|
+
running.add(key)
|
|
749
|
+
setNotice(label)
|
|
750
|
+
void (async () => {
|
|
751
|
+
let message = ''
|
|
752
|
+
try {
|
|
753
|
+
message = String((await work()) ?? '')
|
|
754
|
+
} catch (error) {
|
|
755
|
+
message = String(error?.message ?? error ?? '')
|
|
756
|
+
} finally {
|
|
757
|
+
running.delete(key)
|
|
758
|
+
}
|
|
759
|
+
setNotice(message)
|
|
760
|
+
})()
|
|
761
|
+
return { refresh: true }
|
|
762
|
+
}
|
|
763
|
+
|
|
731
764
|
const panelActions = {
|
|
732
|
-
|
|
765
|
+
refresh(scope, event) {
|
|
733
766
|
const t = makeT(event?.locale)
|
|
734
|
-
|
|
735
|
-
|
|
767
|
+
return runInBackground('refresh', t('pendingRefresh'), async () => {
|
|
768
|
+
await refreshQuota(true)
|
|
769
|
+
await refreshCatalog(true)
|
|
770
|
+
return t('toastQuotaUpdated')
|
|
771
|
+
})
|
|
736
772
|
},
|
|
737
773
|
async 'pick-model'(scope, event) {
|
|
738
774
|
const t = makeT(event?.locale)
|
|
@@ -742,38 +778,38 @@ const modelRowOf = (entry, current, t) => {
|
|
|
742
778
|
? { toast: t('toastModelSet', { model }), refresh: true }
|
|
743
779
|
: { toast: t('toastModelBad') }
|
|
744
780
|
},
|
|
745
|
-
|
|
781
|
+
'test-all'(scope, event) {
|
|
746
782
|
const t = makeT(event?.locale)
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
783
|
+
return runInBackground('test', t('pendingTest'), async () => {
|
|
784
|
+
const keys = pool ? [...pool.keys] : []
|
|
785
|
+
if (keys.length === 0) return t('toastNoKeys')
|
|
786
|
+
// 注意不能用 /v1/models 验 Key:那个接口是公开的,什么令牌都返回 200。
|
|
787
|
+
// 真正会校验令牌的是额度接口,而且查额度不扣魔粒。
|
|
788
|
+
const rows = await quota.refresh(
|
|
789
|
+
keys.map((k) => ({ id: k.id, key: k.key, region: k.region })),
|
|
790
|
+
{ force: true },
|
|
791
|
+
)
|
|
792
|
+
for (const [i, row] of rows.entries()) {
|
|
793
|
+
const k = keys[i]
|
|
794
|
+
if (row.state?.region) await pool.setRegion(k.id, row.state.region)
|
|
795
|
+
if (!row.state?.ok) {
|
|
796
|
+
pool.markFailure(k.id, '令牌无效', String(row.state?.error ?? '').slice(0, 80))
|
|
797
|
+
continue
|
|
798
|
+
}
|
|
799
|
+
if (row.state.available < QUOTA_FLOOR) {
|
|
800
|
+
pool.markFailure(k.id, '额度不足', `剩余 ${row.state.available} 魔粒`, nextQuotaResetAt())
|
|
801
|
+
continue
|
|
802
|
+
}
|
|
803
|
+
pool.restore(k.id)
|
|
765
804
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
if (short > 0) parts.push(t('partShort', { n: short }))
|
|
775
|
-
if (invalid > 0) parts.push(t('partInvalid', { n: invalid }))
|
|
776
|
-
return { toast: t('toastTestDone', { parts: parts.join(' · ') }) }
|
|
805
|
+
const ok = rows.filter((r) => r.state?.ok && r.state.available >= QUOTA_FLOOR).length
|
|
806
|
+
const invalid = rows.filter((r) => !r.state?.ok).length
|
|
807
|
+
const short = rows.length - ok - invalid
|
|
808
|
+
const parts = [t('partOk', { ok, total: keys.length })]
|
|
809
|
+
if (short > 0) parts.push(t('partShort', { n: short }))
|
|
810
|
+
if (invalid > 0) parts.push(t('partInvalid', { n: invalid }))
|
|
811
|
+
return t('toastTestDone', { parts: parts.join(' · ') })
|
|
812
|
+
})
|
|
777
813
|
},
|
|
778
814
|
'restore-all': async (scope, event) => {
|
|
779
815
|
const t = makeT(event?.locale)
|
|
@@ -853,23 +889,26 @@ const modelRowOf = (entry, current, t) => {
|
|
|
853
889
|
})
|
|
854
890
|
return { toast: t('toastDeleted', { id }), refresh: true }
|
|
855
891
|
},
|
|
856
|
-
'add-form'
|
|
892
|
+
'add-form'(scope, event) {
|
|
893
|
+
const t = makeT(event?.locale)
|
|
857
894
|
const value = event?.value
|
|
858
895
|
const raw = typeof value === 'string' ? value : value?.newKey ?? value?.value ?? ''
|
|
859
|
-
|
|
860
|
-
|
|
896
|
+
return runInBackground('add', t('pendingAdd'), async () => {
|
|
897
|
+
const result = await addKey(raw, event?.locale)
|
|
898
|
+
return result?.toast ?? t('toastNoNew')
|
|
899
|
+
})
|
|
861
900
|
},
|
|
862
901
|
'key-back'() {
|
|
863
902
|
return { open: 'panel:pool' }
|
|
864
903
|
},
|
|
865
|
-
|
|
904
|
+
'refresh-models'(scope, event) {
|
|
866
905
|
const t = makeT(event?.locale)
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
906
|
+
return runInBackground('models', t('pendingModels'), async () => {
|
|
907
|
+
await refreshCatalog(true)
|
|
908
|
+
return t('toastModelsReloaded')
|
|
909
|
+
})
|
|
870
910
|
},
|
|
871
911
|
}
|
|
872
|
-
|
|
873
912
|
dshGo.panel('pool', { view: viewPool, data: panelData(buildPoolData), actions: panelActions })
|
|
874
913
|
dshGo.panel('key', { view: viewKey, data: panelData(buildKeyData), actions: panelActions })
|
|
875
914
|
dshGo.panel('models', { view: viewModels, data: panelData(buildModelsData), actions: panelActions })
|
|
@@ -911,11 +950,14 @@ const modelRowOf = (entry, current, t) => {
|
|
|
911
950
|
},
|
|
912
951
|
})
|
|
913
952
|
|
|
914
|
-
dshGo.on('addkey',
|
|
953
|
+
dshGo.on('addkey', (scope, event) => {
|
|
954
|
+
const t = makeT(event?.locale)
|
|
915
955
|
const text = String(scope.clipboard ?? '').trim()
|
|
916
|
-
if (!text) return { toast:
|
|
917
|
-
|
|
918
|
-
|
|
956
|
+
if (!text) return { toast: t('toastNoClipboard') }
|
|
957
|
+
return runInBackground('add', t('pendingAdd'), async () => {
|
|
958
|
+
const result = await addKey(text, event?.locale)
|
|
959
|
+
return result?.toast ?? t('toastNoNew')
|
|
960
|
+
})
|
|
919
961
|
})
|
|
920
962
|
|
|
921
963
|
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.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "魔搭多 Key 聚合网关:无感路由(模型不变,Key 遇限流/额度不足/无权限/不存在自动换下一个);自动识别国际版/国内版令牌;单个 Key 可禁用/停用/恢复/删除/查看;全部密钥分页浏览;模型实际拉取国际版+国内版合并成简化名、按站点分组;额度低于 2 停用到次日 00:01、多 Key 轮换、限流临时失败冷却 5 分钟,并注册为 DSH 模型供应商",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Yizutt",
|