dsh-modelscope-pool 0.8.0 → 0.10.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
@@ -19,6 +19,8 @@
19
19
  - [安装](#安装)
20
20
  - [配置](#配置)
21
21
  - [界面](#界面)
22
+ - [多语言](#多语言)
23
+ - [网页版与桌面版](#网页版与桌面版)
22
24
  - [HTTP 接口](#http-接口)
23
25
  - [接入其它客户端](#接入其它客户端)
24
26
  - [常见问题](#常见问题)
@@ -62,6 +64,17 @@
62
64
 
63
65
  同一个模型只显示一行;请求时按当前 Key 所属站点自动换成那边的真实 id。版本号(`.1`、`-0813`、`-1017`)是模型身份的一部分,**原样保留**。
64
66
 
67
+ ### 界面跟随系统语言
68
+
69
+ 面板、按钮、提示、状态条、模型页里的站点后缀都跟随 App 的语言:
70
+
71
+ | 语言 | 站点叫法 |
72
+ |---|---|
73
+ | 中文 | 国际版 / 国内版 |
74
+ | 其他语言 | International / China |
75
+
76
+ 中文以外的语言不会出现「Domestic」「Mainland」这类别扭说法。存盘的 `keys.json`、`cooldowns.json` 里仍然是语言无关的常量,换语言不会动到数据。
77
+
65
78
  ### 额度保护
66
79
 
67
80
  - 剩余魔粒(ModelScope 的额度单位)**低于 2** 时,Key 自动停用到次日 00:01,避免无谓的 `429`。
@@ -165,7 +178,7 @@ dsh plugin --profile web add /path/to/dsh-modelscope-pool
165
178
 
166
179
  | 面板 | 位置 | 内容 |
167
180
  |---|---|---|
168
- | **魔搭 Key 池** | 对话标签 / 侧栏 / 功能页 | 可用 / 冷却 / 魔粒三个数字、Key 状态扇形图、Key 列表(分页)、刷新额度、测试全部、全部恢复、添加 Key |
181
+ | **魔搭 Key 池** | 对话标签 / 侧栏 / 功能页 | 三个数字(可用 / 冷却 / 魔粒)、一条魔粒进度条、Key 列表(每页 10 个)、刷新额度 / 测试全部 / 全部恢复、添加入口 |
169
182
  | **Key 详情** | 侧栏 / 功能页 | 点列表任意一行进来:完整令牌、站点、额度明细、状态、失败原因,以及禁用 / 停用 / 恢复 / 删除(删除需连点两次确认) |
170
183
  | **魔搭模型** | 对话标签 / 侧栏 / 功能页 | 按「国际版 / 国内版 / 双站」分组的模型列表,点一下设为默认,底部可重新拉取 |
171
184
 
@@ -173,6 +186,48 @@ dsh plugin --profile web add /path/to/dsh-modelscope-pool
173
186
 
174
187
  ---
175
188
 
189
+ ## 网页版与桌面版
190
+
191
+ 原生面板只在 dsh-go 手机端出现。Harness Web / 桌面端没有这套面板,所以插件在宿主自带的 webServer 上开了一个**同源管理页**:
192
+
193
+ ```
194
+ http://127.0.0.1:3080/modelscope-pool
195
+ ```
196
+
197
+ (端口跟着你的 Harness 网页端口走,默认 3080。)
198
+
199
+ 页面上能做的事和原生面板一致:看三个数字和魔粒进度、看 Key 列表、刷新额度、测试全部、全部恢复、逐条禁用 / 停用 / 恢复 / 删除、批量添加 Key、切换默认模型。中英文跟随浏览器语言,右上角可手动切换。
200
+
201
+ 两道保护:
202
+
203
+ - **只认本机回环地址**。来自 `192.168.*`、`10.*` 等局域网地址的请求直接 403,令牌不会因为开了这个页面而被同网段的人看到。
204
+ - **沿用 Harness 自己的登录校验**。这个路径和 `/api/*` 一样要过宿主的 Host/Origin 校验与 cookie 凭据,所以请在你已经登录 DSH 网页的那个浏览器里打开。
205
+
206
+ 桌面端内嵌的就是同一套 Web 界面,地址相同。
207
+
208
+ ## 多语言
209
+
210
+ 界面文案跟随系统语言,判定顺序:
211
+
212
+ 1. App 传下来的 `locale`(面板视图、数据推送、按钮回调都带);
213
+ 2. 没有时用引擎进程的 `Intl.DateTimeFormat().resolvedOptions().locale`;
214
+ 3. 再没有就用 `LANG` 环境变量;
215
+ 4. 都没有则按中文显示。
216
+
217
+ 要加一门语言,只改 `lib/i18n.js`:`S` 里每一条都是 `[中文, 英文]`,加第三列即可(`makeT` 里的 `idx` 也要跟着扩展)。
218
+
219
+ ## 魔粒用量怎么算
220
+
221
+ 魔搭按「魔粒」(magicubes)计费,每个 Key 的免费额度是 **250 魔粒**。面板上的魔粒进度条(网页版是顶部那条横条)就是按这个估的:
222
+
223
+ ```
224
+ 总魔粒 = Key 数量 × 250
225
+ 剩余 = 各 Key 剩余之和(还没查到的 Key 按满额 250 计)
226
+ 已用 = 总魔粒 − 剩余
227
+ ```
228
+
229
+ > 这是**估算**,不是官方账单。还没刷新过额度的 Key 会被当成满额,所以「已用」只会偏小、不会虚高。要准确数字就点「刷新额度」,或直接看每行 Key 的剩余魔粒。
230
+
176
231
  ## HTTP 接口
177
232
 
178
233
  全部为 OpenAI 兼容格式。
@@ -296,8 +351,14 @@ node "$DSH_HOME/skills/dsh-go-plugin/kit/bin/check.mjs" . --load
296
351
 
297
352
  ---
298
353
 
299
- ## License
354
+ ## 声明
355
+
356
+ - 本项目与**魔搭(ModelScope)官方无关**,不是官方客户端,也未获官方认可或背书。
357
+ - 只是跑在本机的一个客户端侧聚合工具:所有请求仍由你自己的 Key 直接发往魔搭官方接口,作者不经手、不中转任何流量。
358
+ - 「魔粒」额度、站点归属、模型列表都以魔搭官方返回为准;本项目里的 250 魔粒/Key 是**估算值**,仅用于界面展示,不构成任何额度承诺。
359
+ - 请自行阅读并遵守 [ModelScope 服务条款](https://modelscope.cn) 与各模型自己的许可条款。因违反条款、超额调用或令牌泄露造成的后果由使用者自负。
360
+ - 软件按 MIT 许可「按原样」提供,不附带任何明示或默示担保。
300
361
 
301
- [MIT](LICENSE) © 2026 Yizutt
362
+ ## License
302
363
 
303
- 本项目与 ModelScope(魔搭)官方无关,只是本机的客户端侧聚合工具。使用前请自行阅读并遵守 [ModelScope 服务条款](https://modelscope.cn)。
364
+ [MIT](LICENSE) © 2026 Yizutt
package/lib/i18n.js ADDED
@@ -0,0 +1,233 @@
1
+ /**
2
+ * 界面多语言:跟随 App 的语言(`caps.locale` / `event.locale`),缺省按系统语言判断。
3
+ *
4
+ * 约定:中文用「国际版 / 国内版」,其他语言用「International / China」。
5
+ * 站点名、失败原因这类存盘的字符串保持中文常量,显示时再翻译,
6
+ * 这样 keys.json / cooldowns.json 的数据不受语言影响。
7
+ */
8
+
9
+ const S = {
10
+ // 面板标题
11
+ poolTitle: ['魔搭 Key 池', 'ModelScope keys'],
12
+ keyPanelTitle: ['Key 详情', 'Key detail'],
13
+ modelsPanelTitle: ['魔搭模型', 'ModelScope models'],
14
+
15
+ // 概览
16
+ metricReady: ['可用', 'Ready'],
17
+ metricCooling: ['冷却', 'Cooling'],
18
+ metricMagicubes: ['魔粒', 'Magicubes'],
19
+
20
+ // 按钮
21
+ btnPrev: ['上一页', 'Prev'],
22
+ btnNext: ['下一页', 'Next'],
23
+ btnRefresh: ['刷新额度', 'Refresh quota'],
24
+ btnTestAll: ['测试全部', 'Test all'],
25
+ btnRestoreAll: ['全部恢复', 'Restore all'],
26
+ btnDisable: ['禁用', 'Disable'],
27
+ btnStop: ['停用', 'Pause'],
28
+ btnRestore: ['恢复', 'Restore'],
29
+ btnDelete: ['删除', 'Delete'],
30
+ btnBack: ['返回 Key 列表', 'Back to keys'],
31
+ btnReloadModels: ['重新拉取模型', 'Reload models'],
32
+ formSubmit: ['添加', 'Add'],
33
+ fieldLabel: ['添加 Key', 'Add key'],
34
+ fieldPlaceholder: ['粘贴 ms- 开头的令牌,可多行', 'Paste ms- tokens, one per line'],
35
+
36
+ // 提示
37
+ hintNoKeys: [
38
+ '还没有 Key:在魔搭复制令牌(ms- 开头)后点输入框上方的「添加 Key」,也可以在下面直接填。',
39
+ 'No keys yet. Copy a token that starts with ms- on ModelScope, then tap "Add key" above the composer — or paste it below.',
40
+ ],
41
+ hintCooling: [
42
+ '无感切换已开:剩余魔粒低于 2 的 Key 已自动停用到次日 00:01,避免撞 429。到点自动恢复。',
43
+ 'Silent rotation is on. Keys with fewer than 2 magicubes are paused until 00:01 next day to avoid 429s, and resume automatically.',
44
+ ],
45
+ hintModel: [
46
+ '无感切换已开:模型始终是 {model},Key 遇到限流、额度不足、没权限会自动换下一个;魔粒低于 2 的 Key 会提前停用。',
47
+ 'Silent rotation is on. The model stays {model}; on rate limits, exhausted quota or missing permission the gateway moves to the next key. Keys under 2 magicubes are paused early.',
48
+ ],
49
+ hintIdle: [
50
+ '点「刷新额度」看剩余魔粒;点下面的模型可以设为默认。',
51
+ 'Tap "Refresh quota" to check remaining magicubes; tap a model below to set it as default.',
52
+ ],
53
+ routed: [
54
+ '已记下 {n} 个「Key×模型」的失败组合(5 分钟内自动跳过)。',
55
+ '{n} key/model failures remembered and skipped for 5 minutes.',
56
+ ],
57
+ hintKeyDetail: [
58
+ '「禁用」= 一直停到点恢复;「停用」= 停到北京时间次日 00:01;「删除」会把令牌从本机文件里去掉。',
59
+ '"Disable" keeps the key off until you restore it; "Pause" stops it until 00:01 next day (Beijing time); "Delete" removes the token from this device.',
60
+ ],
61
+ hintKeyFallback: [
62
+ '回到「魔搭 Key 池」面板,点列表里任意一行就会选中它。',
63
+ 'Go back to the key list and tap a row to select it.',
64
+ ],
65
+
66
+ // 扇形图
67
+ bucketReady: ['可用', 'Ready'],
68
+ bucketLow: ['额度不足', 'Out of quota'],
69
+ bucketInvalid: ['令牌无效', 'Invalid token'],
70
+ bucketOther: ['其他冷却', 'Cooling (other)'],
71
+ unitKey: ['个', ''],
72
+ captionNoKeys: ['还没有 Key', 'No keys yet'],
73
+ shareTitle: ['Key 状态分布', 'Key status'],
74
+ magicLabel: ['魔粒用量', 'Magicube usage'],
75
+ magicBarCaption: ['剩余 {left} / 共 {total}', '{left} left of {total}'],
76
+ magicLeft: ['剩余', 'Remaining'],
77
+ magicUsed: ['已用', 'Spent'],
78
+ magicCaption: [
79
+ '共 {total} 魔粒({keys} 个 Key × {per});剩余 {left},已用 {used}。未查到的 Key 按满额计。',
80
+ '{total} magicubes total ({keys} keys × {per}); {left} left, {used} spent. Keys not checked yet count as full.',
81
+ ],
82
+ magicCaptionEmpty: ['还没有 Key,估不出总魔粒', 'No keys yet, so the magicube total is unknown'],
83
+ captionTotal: [
84
+ '共 {total} 个 Key:可用 {ok},额度不足 {low},其余冷却中',
85
+ '{total} keys: {ok} ready, {low} out of quota, the rest cooling down',
86
+ ],
87
+
88
+ // Key 列表
89
+ keyTitleEmpty: ['Key 池(还没有 Key)', 'Key pool (empty)'],
90
+ keyTitlePage: [
91
+ 'Key 池 {total} 个 · 第 {page}/{pages} 页(点一行看详情)',
92
+ '{total} keys · page {page}/{pages} (tap a row for detail)',
93
+ ],
94
+ keyPager: ['第 {page}/{pages} 页 · 每页 {size} 个', 'Page {page}/{pages} · {size} per page'],
95
+ usedTimes: ['用过 {n} 次', 'used {n}×'],
96
+ quotaLeft: ['额度 {n} 魔粒', '{n} magicubes'],
97
+ quotaUnknown: ['额度未知({error})', 'quota unknown ({error})'],
98
+ quotaUnchecked: ['额度未查', 'quota not checked'],
99
+ selectedMark: ['(已选中)', ' (selected)'],
100
+ failTimes: ['失败 {n} 次', '{n} failed'],
101
+ stateReady: ['可用', 'Ready'],
102
+ stateDisabled: ['已禁用', 'Disabled'],
103
+ coolingUntil: ['冷却至 {time}', 'cooling until {time}'],
104
+ coolingLeft: ['冷却 {time}', 'cooling {time}'],
105
+
106
+ // Key 详情
107
+ focusEmpty: ['还没选中 Key(回「Key 列表」点一行)', 'No key selected — tap a row in the key list'],
108
+ rowToken: ['完整令牌', 'Full token'],
109
+ rowSite: ['站点', 'Site'],
110
+ rowQuota: ['额度', 'Quota'],
111
+ rowState: ['状态', 'State'],
112
+ rowFails: ['失败', 'Failures'],
113
+ quotaDetail: ['{n} 魔粒(总额 {total},冻结 {frozen})', '{n} magicubes (total {total}, frozen {frozen})'],
114
+ quotaFailed: ['查询失败:{error}', 'lookup failed: {error}'],
115
+ quotaNever: ['还没查过', 'not checked yet'],
116
+ stateManual: ['已禁用(点「恢复」放开)', 'Disabled — tap "Restore" to re-enable'],
117
+ failsDetail: ['{n} 次 · {error}{used}', '{n}× · {error}{used}'],
118
+ usedSuffix: [' · 用过 {n} 次', ' · used {n}×'],
119
+ none: ['无', 'none'],
120
+
121
+ // 模型列表
122
+ modelsTitleEmpty: ['模型(还没读到,点下面「重新拉取模型」)', 'Models (not loaded — tap "Reload models")'],
123
+ modelsTitleCount: ['{n} 个模型 · 当前 {current}', '{n} models · current {current}'],
124
+ modelNotPicked: ['未选', 'none'],
125
+ groupCount: ['{n} 个模型', '{n} models'],
126
+ groupBoth: ['国际版 + 国内版都有', 'On both sites'],
127
+ groupUnknown: ['站点未知', 'Unknown site'],
128
+ currentDefault: ['当前默认', 'current default'],
129
+ trailingCurrent: ['当前', 'Current'],
130
+ trailingUse: ['使用', 'Use'],
131
+
132
+ // 操作反馈
133
+ toastQuotaUpdated: ['额度已更新', 'Quota refreshed'],
134
+ toastModelSet: ['默认模型:{model}', 'Default model: {model}'],
135
+ toastModelBad: ['模型名不可用', 'That model name is not usable'],
136
+ toastNoKeys: ['池里还没有 Key', 'The pool has no keys'],
137
+ toastTestDone: ['测试完成:{parts}', 'Test done: {parts}'],
138
+ partOk: ['{ok}/{total} 个 Key 可用', '{ok}/{total} keys ready'],
139
+ partShort: ['{n} 个额度不足', '{n} out of quota'],
140
+ partInvalid: ['{n} 个令牌无效', '{n} invalid'],
141
+ toastRestored: ['已把全部 Key 恢复为可用', 'All keys restored'],
142
+ toastNoPick: ['先在上面点一行选中一个 Key', 'Select a key from the list first'],
143
+ toastNoKeyId: ['没认出这个 Key', 'Could not identify that key'],
144
+ toastDisabled: ['{id} 已禁用,点「恢复」才放开', '{id} disabled — tap "Restore" to re-enable'],
145
+ toastStopped: ['{id} 停用到北京时间次日 00:01', '{id} paused until 00:01 next day (Beijing time)'],
146
+ toastRestoredOne: ['{id} 已恢复可用', '{id} is ready again'],
147
+ toastDeleteArm: ['再点一次「删除」确认删掉 {id}', 'Tap "Delete" once more to remove {id}'],
148
+ toastDeleted: ['{id} 已从本机删除', '{id} removed from this device'],
149
+ toastFirstPage: ['已经是第一页了', 'Already on the first page'],
150
+ toastLastPage: ['已经是最后一页了', 'Already on the last page'],
151
+ toastModelsReloaded: ['模型列表已重新拉取', 'Model list reloaded'],
152
+ toastNoClipboard: ['剪贴板里没有文字', 'Nothing to read in the clipboard'],
153
+ toastNoInput: ['没读到 Key', 'No key found in the input'],
154
+ toastNotMs: ['这不像魔搭令牌(应以 ms- 开头)', 'That does not look like a ModelScope token (should start with ms-)'],
155
+ toastNoNew: ['没有新增 Key', 'No new keys added'],
156
+
157
+ // 添加结果
158
+ addDone: ['新增 {n} 个', '{n} added'],
159
+ addIntl: ['国际版 {n}', 'International {n}'],
160
+ addCn: ['国内版 {n}', 'China {n}'],
161
+ addLow: ['{n} 个额度不足已停用', '{n} out of quota, paused'],
162
+ addInvalid: ['{n} 个令牌无效', '{n} invalid'],
163
+ addSkipped: ['{n} 个重复跳过', '{n} duplicates skipped'],
164
+ joinArrow: ['|', ' · '],
165
+
166
+ // 状态条
167
+ stripEmpty: ['魔搭 未配置', 'ModelScope · none'],
168
+ stripText: ['魔搭 {ok}/{total} · {quota}', 'ModelScope {ok}/{total} · {quota}'],
169
+ stripQuota: ['{n} 魔粒', '{n} left'],
170
+ stripQuotaUnknown: ['额度?', 'quota?'],
171
+
172
+ // 站点与原因
173
+ regionAi: ['国际版', 'International'],
174
+ regionCn: ['国内版', 'China'],
175
+ regionUnknown: ['版本未知', 'Unknown site'],
176
+ }
177
+
178
+ const REASONS = {
179
+ 额度不足: ['额度不足', 'Out of quota'],
180
+ 额度用尽: ['额度用尽', 'Quota exhausted'],
181
+ 令牌无效: ['令牌无效', 'Invalid token'],
182
+ 被限流: ['被限流', 'Rate limited'],
183
+ 模型名不可用: ['模型名不可用', 'Model unavailable'],
184
+ 上游超时: ['上游超时', 'Upstream timeout'],
185
+ 读取请求失败: ['读取请求失败', 'Request failed'],
186
+ 冷却中: ['冷却中', 'Cooling down'],
187
+ 手动禁用: ['手动禁用', 'Disabled manually'],
188
+ 手动停用: ['手动停用', 'Paused manually'],
189
+ 其他冷却: ['其他冷却', 'Cooling (other)'],
190
+ }
191
+
192
+ /** 系统语言判断:App 没给 locale 时的兜底。 */
193
+ export function detectLang() {
194
+ try {
195
+ const locale = Intl.DateTimeFormat().resolvedOptions().locale
196
+ if (locale) return String(locale).toLowerCase().startsWith('zh') ? 'zh' : 'en'
197
+ } catch {}
198
+ const env = String(process.env.LC_ALL ?? process.env.LC_MESSAGES ?? process.env.LANG ?? '')
199
+ if (env) return env.toLowerCase().startsWith('zh') ? 'zh' : 'en'
200
+ return 'zh'
201
+ }
202
+
203
+ /** `zh-Hans-CN` / `en-US` / 空 → `'zh'` | `'en'`。 */
204
+ export function pickLang(locale) {
205
+ const raw = String(locale ?? '').trim()
206
+ if (!raw) return detectLang()
207
+ return raw.toLowerCase().startsWith('zh') ? 'zh' : 'en'
208
+ }
209
+
210
+ /**
211
+ * 造一个翻译函数:`t('key', { n: 3 })`,另带 `t.lang`、`t.region(r)`、`t.reason(r)`。
212
+ */
213
+ export function makeT(locale) {
214
+ const lang = pickLang(locale)
215
+ const idx = lang === 'en' ? 1 : 0
216
+ const t = (key, vars) => {
217
+ const row = S[key]
218
+ let text = row ? row[idx] : key
219
+ if (vars) {
220
+ for (const k of Object.keys(vars)) text = text.split(`{${k}}`).join(String(vars[k]))
221
+ }
222
+ return text
223
+ }
224
+ t.lang = lang
225
+ t.region = (region) =>
226
+ region === 'ai' ? t('regionAi') : region === 'cn' ? t('regionCn') : t('regionUnknown')
227
+ t.reason = (reason) => {
228
+ const text = String(reason ?? '')
229
+ const row = REASONS[text]
230
+ return row ? row[idx] : text
231
+ }
232
+ return t
233
+ }