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 +65 -4
- package/lib/i18n.js +233 -0
- package/lib/index.js +450 -202
- package/lib/provider.js +8 -2
- package/lib/web.js +287 -0
- package/package.json +5 -1
package/lib/provider.js
CHANGED
|
@@ -5,6 +5,8 @@ import { createRequire } from 'node:module'
|
|
|
5
5
|
import { pathToFileURL } from 'node:url'
|
|
6
6
|
import { join } from 'node:path'
|
|
7
7
|
|
|
8
|
+
import { detectLang } from './i18n.js'
|
|
9
|
+
|
|
8
10
|
const HOST_ROOTS = ['/opt/dsh', process.cwd()]
|
|
9
11
|
const DEFAULT_CONTEXT_WINDOW = 131072
|
|
10
12
|
const IMAGE_BUDGETS = {
|
|
@@ -75,7 +77,11 @@ const INERT_AUTH = {
|
|
|
75
77
|
* @param options.getEntries 带版本/短名的模型条目(优先于 getModels)
|
|
76
78
|
* @param options.entryId 本插件在 loader 里的条目 id
|
|
77
79
|
*/
|
|
78
|
-
|
|
80
|
+
// 模型页里的站点后缀跟随系统语言:中文用「国际版 / 国内版」,其他语言用「International / China」。
|
|
81
|
+
const REGION_TAG = () =>
|
|
82
|
+
detectLang() === 'en'
|
|
83
|
+
? { ai: 'International', cn: 'China' }
|
|
84
|
+
: { ai: '国际版', cn: '国内版' }
|
|
79
85
|
|
|
80
86
|
/** 模型页里显示的名字:短名 + 版本,例如「DeepSeek-V4-Flash-1017 · 国际版」。 */
|
|
81
87
|
const displayNameOf = (entry) => {
|
|
@@ -83,7 +89,7 @@ const displayNameOf = (entry) => {
|
|
|
83
89
|
const regions = Array.isArray(entry?.regions) ? entry.regions : []
|
|
84
90
|
// 两个站点都有就不加后缀,避免模型页里一堆「· 双站」。
|
|
85
91
|
if (regions.length === 0 || regions.length > 1) return base
|
|
86
|
-
const tag = REGION_TAG[regions[0]] ?? ''
|
|
92
|
+
const tag = REGION_TAG()[regions[0]] ?? ''
|
|
87
93
|
return tag ? `${base} · ${tag}` : base
|
|
88
94
|
}
|
|
89
95
|
|
package/lib/web.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// 网页版 / 桌面版 DSH 的管理页面。
|
|
2
|
+
// dsh-go 手机端用原生面板,Harness Web(含桌面端内嵌的同一套界面)没有面板,
|
|
3
|
+
// 所以在宿主自带的 webServer 上开一个同源页面:http://127.0.0.1:<web 端口>/modelscope-pool
|
|
4
|
+
// 页面只读同源 JSON,不跨域;只接受本机回环地址,避免局域网里被人看到令牌。
|
|
5
|
+
|
|
6
|
+
const PAGE_PATH = '/modelscope-pool'
|
|
7
|
+
|
|
8
|
+
const HTML = [
|
|
9
|
+
'<!doctype html>',
|
|
10
|
+
'<html lang="zh-CN"><head>',
|
|
11
|
+
'<meta charset="utf-8">',
|
|
12
|
+
'<meta name="viewport" content="width=device-width,initial-scale=1">',
|
|
13
|
+
'<title>ModelScope key pool</title>',
|
|
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">',
|
|
55
|
+
'<button class="pri" data-act="refresh" id="b-refresh"></button>',
|
|
56
|
+
'<button data-act="test" id="b-test"></button>',
|
|
57
|
+
'<button data-act="restoreAll" id="b-restore"></button>',
|
|
58
|
+
'<button id="b-lang"></button>',
|
|
59
|
+
'</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>',
|
|
69
|
+
'<div class="toast" id="toast"></div>',
|
|
70
|
+
'<script>',
|
|
71
|
+
'var L=null,S=null,PAGE=0,PER=20,ARM="";',
|
|
72
|
+
'var DICT={',
|
|
73
|
+
' zh:{title:"魔搭 Key 池",sub:"本页只在 127.0.0.1 上可用;Key 不会离开这台设备。",',
|
|
74
|
+
' ok:"可用",cool:"冷却",magic:"魔粒",left:"剩余",total:"共",refresh:"刷新额度",test:"测试全部",restoreAll:"全部恢复",',
|
|
75
|
+
' prev:"上一页",next:"下一页",add:"添加",addSum:"添加 Key(一行一个)",modelSum:"默认模型",',
|
|
76
|
+
' id:"Key",site:"站点",state:"状态",quota:"额度",ops:"操作",',
|
|
77
|
+
' disable:"禁用",stop:"停用",restore:"恢复",del:"删除",delAgain:"再点一次",',
|
|
78
|
+
' ai:"国际版",cn:"国内版",none:"—",empty:"还没有 Key",',
|
|
79
|
+
' copied:"已刷新",added:"已添加",saved:"已保存",failed:"操作失败",lang:"English"},',
|
|
80
|
+
' en:{title:"ModelScope keys",sub:"This page only answers on 127.0.0.1; keys never leave this device.",',
|
|
81
|
+
' ok:"Ready",cool:"Cooling",magic:"Magicubes",left:"left",total:"of",refresh:"Refresh quota",test:"Test all",restoreAll:"Restore all",',
|
|
82
|
+
' prev:"Prev",next:"Next",add:"Add",addSum:"Add keys (one per line)",modelSum:"Default model",',
|
|
83
|
+
' id:"Key",site:"Site",state:"State",quota:"Quota",ops:"Actions",',
|
|
84
|
+
' disable:"Disable",stop:"Pause",restore:"Restore",del:"Delete",delAgain:"Tap again",',
|
|
85
|
+
' ai:"International",cn:"China",none:"—",empty:"No keys yet",',
|
|
86
|
+
' copied:"Refreshed",added:"Added",saved:"Saved",failed:"Action failed",lang:"中文"}};',
|
|
87
|
+
'var RSN={"额度不足":["额度不足","Out of quota"],"额度用尽":["额度用尽","Quota exhausted"],"令牌无效":["令牌无效","Invalid token"],"被限流":["被限流","Rate limited"],"模型名不可用":["模型名不可用","Model unavailable"],"上游超时":["上游超时","Upstream timeout"],"读取请求失败":["读取请求失败","Request failed"],"冷却中":["冷却中","Cooling down"],"手动禁用":["手动禁用","Disabled"],"手动停用":["手动停用","Paused"]};',
|
|
88
|
+
'function rsn(r){if(!r)return "";var x=RSN[r];return x?(L===DICT.en?x[1]:x[0]):r}',
|
|
89
|
+
'function t(k){return (L&&L[k])||k}',
|
|
90
|
+
'function pickLang(){try{var s=localStorage.getItem("ms-lang");if(s==="zh"||s==="en")return s}catch(e){}',
|
|
91
|
+
' var n=(navigator.language||"zh").toLowerCase();return n.indexOf("zh")===0?"zh":"en"}',
|
|
92
|
+
'function setLang(x){L=DICT[x];try{localStorage.setItem("ms-lang",x)}catch(e){}',
|
|
93
|
+
' document.documentElement.lang=x==="zh"?"zh-CN":"en";',
|
|
94
|
+
' document.querySelectorAll("[id^=b-lang]").forEach(function(b){b.textContent=L.lang});render()}',
|
|
95
|
+
'function toast(m){var el=document.getElementById("toast");el.textContent=m;el.classList.add("on");',
|
|
96
|
+
' clearTimeout(window.__t);window.__t=setTimeout(function(){el.classList.remove("on")},2200)}',
|
|
97
|
+
'function fmt(ms){if(!ms)return "";var d=new Date(ms);function p(n){return n<10?"0"+n:""+n}',
|
|
98
|
+
' return p(d.getMonth()+1)+"-"+p(d.getDate())+" "+p(d.getHours())+":"+p(d.getMinutes())}',
|
|
99
|
+
'function el(tag,cls,text){var e=document.createElement(tag);if(cls)e.className=cls;if(text!=null)e.textContent=text;return e}',
|
|
100
|
+
'function render(){',
|
|
101
|
+
' if(!S||!L)return;',
|
|
102
|
+
' document.getElementById("title").textContent=L.title;',
|
|
103
|
+
' document.getElementById("sub").textContent=L.sub;',
|
|
104
|
+
' document.getElementById("s-ok").textContent=S.available;',
|
|
105
|
+
' document.getElementById("s-cool").textContent=S.cooling;',
|
|
106
|
+
' document.getElementById("s-magic").textContent=S.magic.left;',
|
|
107
|
+
' document.getElementById("l-ok").textContent=L.ok;',
|
|
108
|
+
' document.getElementById("l-cool").textContent=L.cool;',
|
|
109
|
+
' document.getElementById("l-magic").textContent=L.magic;',
|
|
110
|
+
' var pct=S.magic.total>0?Math.max(0,Math.min(100,S.magic.left/S.magic.total*100)):0;',
|
|
111
|
+
' document.getElementById("bar").style.width=pct.toFixed(1)+"%";',
|
|
112
|
+
' document.getElementById("magic-cap").textContent=" "+L.left+" "+S.magic.left+" / "+L.total+" "+S.magic.total;',
|
|
113
|
+
' document.getElementById("b-refresh").textContent=L.refresh;',
|
|
114
|
+
' document.getElementById("b-test").textContent=L.test;',
|
|
115
|
+
' document.getElementById("b-restore").textContent=L.restoreAll;',
|
|
116
|
+
' document.getElementById("b-prev").textContent=L.prev;',
|
|
117
|
+
' document.getElementById("b-next").textContent=L.next;',
|
|
118
|
+
' document.getElementById("b-add").textContent=L.add;',
|
|
119
|
+
' document.getElementById("add-sum").textContent=L.addSum;',
|
|
120
|
+
' document.getElementById("model-sum").textContent=L.modelSum;',
|
|
121
|
+
' var head=document.getElementById("head");head.innerHTML="";',
|
|
122
|
+
' [L.id,L.site,L.state,L.quota,L.ops].forEach(function(h){head.appendChild(el("th",null,h))});',
|
|
123
|
+
' var body=document.getElementById("rows");body.innerHTML="";',
|
|
124
|
+
' var maxPage=Math.max(0,Math.ceil(S.keys.length/PER)-1);PAGE=Math.min(PAGE,maxPage);',
|
|
125
|
+
' var slice=S.keys.slice(PAGE*PER,PAGE*PER+PER);',
|
|
126
|
+
' if(!slice.length){var tr=el("tr");var td=el("td","muted",L.empty);td.colSpan=5;tr.appendChild(td);body.appendChild(tr)}',
|
|
127
|
+
' slice.forEach(function(k){',
|
|
128
|
+
' var tr=el("tr");',
|
|
129
|
+
' tr.appendChild(el("td","mono",k.id+" "+k.masked));',
|
|
130
|
+
' tr.appendChild(el("td",null,k.region==="ai"?L.ai:k.region==="cn"?L.cn:L.none));',
|
|
131
|
+
' var st=el("td",k.state==="ok"?"ok":k.manual?"bad":"warn");',
|
|
132
|
+
' st.textContent=k.state==="ok"?L.ok:(k.manual?L.disable:rsn(k.reason)||L.cool)+(k.untilAt?" "+fmt(k.untilAt):"");',
|
|
133
|
+
' tr.appendChild(st);',
|
|
134
|
+
' var q=el("td",null,k.quota&&k.quota.ok?(k.quota.available+" / "+k.quota.total):L.none);',
|
|
135
|
+
' tr.appendChild(q);',
|
|
136
|
+
' var ops=el("td","ops");',
|
|
137
|
+
' function mk(act,label,cls){var b=el("button",cls,label);b.onclick=function(){act(k.id,label)};ops.appendChild(b);return b}',
|
|
138
|
+
' if(k.state==="ok"||k.manual){mk(actDisable,L.disable)} else {mk(actRestore,L.restore)}',
|
|
139
|
+
' mk(actStop,L.stop);',
|
|
140
|
+
' var delBtn=mk(actDelete,ARM===k.id?L.delAgain:L.del);',
|
|
141
|
+
' tr.appendChild(ops);',
|
|
142
|
+
' body.appendChild(tr);',
|
|
143
|
+
' });',
|
|
144
|
+
' var sel=document.getElementById("model-sel");sel.innerHTML="";',
|
|
145
|
+
' S.models.slice(0,400).forEach(function(m){var o=el("option",null,m.name);o.value=m.id;if(m.id===S.model)o.selected=true;sel.appendChild(o)});',
|
|
146
|
+
'}',
|
|
147
|
+
'function post(action,payload){',
|
|
148
|
+
' var body=Object.assign({action:action},payload||{});',
|
|
149
|
+
' return fetch("' + PAGE_PATH + '/api/action",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(body)})',
|
|
150
|
+
' .then(function(r){return r.json()}).then(function(j){if(j.message)toast(j.message);return load()})',
|
|
151
|
+
' .catch(function(){toast(L.failed)})',
|
|
152
|
+
'}',
|
|
153
|
+
'function actDisable(id){post("disable",{id:id})}',
|
|
154
|
+
'function actStop(id){post("stop",{id:id})}',
|
|
155
|
+
'function actRestore(id){post("restore",{id:id})}',
|
|
156
|
+
'function actDelete(id){if(ARM!==id){ARM=id;render();return}ARM="";post("remove",{id:id})}',
|
|
157
|
+
'function load(){',
|
|
158
|
+
' return fetch("' + PAGE_PATH + '/api/state").then(function(r){if(!r.ok)throw 0;return r.json()})',
|
|
159
|
+
' .then(function(j){S=j;render()}).catch(function(){})',
|
|
160
|
+
'}',
|
|
161
|
+
'document.addEventListener("click",function(e){',
|
|
162
|
+
' var a=e.target&&e.target.getAttribute&&e.target.getAttribute("data-act");',
|
|
163
|
+
' if(a){post(a);return}',
|
|
164
|
+
' if(e.target.id==="b-prev"){if(PAGE>0){PAGE--;render()}return}',
|
|
165
|
+
' if(e.target.id==="b-next"){if((PAGE+1)*PER<S.keys.length){PAGE++;render()}return}',
|
|
166
|
+
' if(e.target.id==="b-add"){var v=document.getElementById("add-text").value;',
|
|
167
|
+
' if(!v.trim())return;post("add",{value:v}).then(function(){document.getElementById("add-text").value=""});return}',
|
|
168
|
+
' if(e.target.id==="b-lang"){setLang(L===DICT.zh?"en":"zh");return}',
|
|
169
|
+
'});',
|
|
170
|
+
'document.addEventListener("change",function(e){',
|
|
171
|
+
' if(e.target.id==="model-sel")post("setModel",{value:e.target.value});',
|
|
172
|
+
'});',
|
|
173
|
+
'setLang(pickLang());load();setInterval(load,4000);',
|
|
174
|
+
'</script></body></html>',
|
|
175
|
+
].join('\n')
|
|
176
|
+
|
|
177
|
+
const isLoopback = (req) => {
|
|
178
|
+
const ip = String(req.socket?.remoteAddress ?? '')
|
|
179
|
+
return ip === '::1' || ip.startsWith('127.') || ip.startsWith('::ffff:127.')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const send = (res, status, type, body, extra) => {
|
|
183
|
+
res.writeHead(status, {
|
|
184
|
+
'content-type': type,
|
|
185
|
+
'cache-control': 'no-store',
|
|
186
|
+
'x-content-type-options': 'nosniff',
|
|
187
|
+
...extra,
|
|
188
|
+
})
|
|
189
|
+
res.end(body)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const sendJson = (res, status, value) => send(res, status, 'application/json; charset=utf-8', JSON.stringify(value))
|
|
193
|
+
|
|
194
|
+
const readBody = (req) =>
|
|
195
|
+
new Promise((resolve) => {
|
|
196
|
+
let size = 0
|
|
197
|
+
const chunks = []
|
|
198
|
+
req.on('data', (chunk) => {
|
|
199
|
+
size += chunk.length
|
|
200
|
+
if (size > 256 * 1024) {
|
|
201
|
+
req.destroy()
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
chunks.push(chunk)
|
|
205
|
+
})
|
|
206
|
+
req.on('end', () => {
|
|
207
|
+
const text = Buffer.concat(chunks).toString('utf8')
|
|
208
|
+
let value = null
|
|
209
|
+
try {
|
|
210
|
+
value = text ? JSON.parse(text) : null
|
|
211
|
+
} catch {}
|
|
212
|
+
resolve(value)
|
|
213
|
+
})
|
|
214
|
+
req.on('error', () => resolve(null))
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 在宿主的 webServer 上挂一个同源管理页。
|
|
219
|
+
* @param webServer 宿主注入的 webServer 服务
|
|
220
|
+
* @param options.getState () => 状态对象
|
|
221
|
+
* @param options.doAction (action, payload) => Promise<string> 返回一行提示
|
|
222
|
+
*/
|
|
223
|
+
export function registerWebAdmin(webServer, { getState, doAction, logger }) {
|
|
224
|
+
const route = {
|
|
225
|
+
kind: 'prefix',
|
|
226
|
+
path: PAGE_PATH,
|
|
227
|
+
handler: async (req, res) => {
|
|
228
|
+
if (!isLoopback(req)) {
|
|
229
|
+
sendJson(res, 403, { error: 'loopback only' })
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
const url = new URL(String(req.url ?? '/'), 'http://127.0.0.1')
|
|
233
|
+
const rest = url.pathname.slice(PAGE_PATH.length).replace(/\/+$/, '')
|
|
234
|
+
|
|
235
|
+
if (rest === '' || rest === '/') {
|
|
236
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
237
|
+
sendJson(res, 405, { error: 'method not allowed' })
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
send(res, 200, 'text/html; charset=utf-8', req.method === 'HEAD' ? '' : HTML, {
|
|
241
|
+
'x-frame-options': 'SAMEORIGIN',
|
|
242
|
+
'referrer-policy': 'no-referrer',
|
|
243
|
+
})
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (rest === '/api/state') {
|
|
248
|
+
if (req.method !== 'GET') {
|
|
249
|
+
sendJson(res, 405, { error: 'method not allowed' })
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
sendJson(res, 200, getState())
|
|
254
|
+
} catch (error) {
|
|
255
|
+
logger?.warn?.(`modelscope-pool: 网页状态读取失败 ${String(error?.message ?? error)}`)
|
|
256
|
+
sendJson(res, 500, { error: 'state failed' })
|
|
257
|
+
}
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (rest === '/api/action') {
|
|
262
|
+
if (req.method !== 'POST') {
|
|
263
|
+
sendJson(res, 405, { error: 'method not allowed' })
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
const body = await readBody(req)
|
|
267
|
+
if (!body || typeof body.action !== 'string') {
|
|
268
|
+
sendJson(res, 400, { error: 'bad request' })
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
const message = await doAction(body.action, body)
|
|
273
|
+
sendJson(res, 200, { ok: true, message: message ?? '' })
|
|
274
|
+
} catch (error) {
|
|
275
|
+
logger?.warn?.(`modelscope-pool: 网页操作失败 ${String(error?.message ?? error)}`)
|
|
276
|
+
sendJson(res, 200, { ok: false, message: String(error?.message ?? error) })
|
|
277
|
+
}
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
sendJson(res, 404, { error: 'not found' })
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
return webServer.register(route)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export const WEB_ADMIN_PATH = PAGE_PATH
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-modelscope-pool",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "魔搭多 Key 聚合网关:无感路由(模型不变,Key 遇限流/额度不足/无权限/不存在自动换下一个);自动识别国际版/国内版令牌;单个 Key 可禁用/停用/恢复/删除/查看;全部密钥分页浏览;模型实际拉取国际版+国内版合并成简化名、按站点分组;额度低于 2 停用到次日 00:01、多 Key 轮换、限流临时失败冷却 5 分钟,并注册为 DSH 模型供应商",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Yizutt",
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/Yizutt/dsh-modelscope-pool.git"
|
|
10
10
|
},
|
|
11
|
+
"homepage": "https://github.com/Yizutt/dsh-modelscope-pool",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Yizutt/dsh-modelscope-pool/issues"
|
|
14
|
+
},
|
|
11
15
|
"keywords": [
|
|
12
16
|
"dsh",
|
|
13
17
|
"dsh-go",
|