dsh-connect-modelscope-token-plan 0.1.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/CHANGELOG.md +198 -0
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/assets/icon-preview.png +0 -0
- package/assets/tab-account-and-usage.png +0 -0
- package/assets/tab-model-list.png +0 -0
- package/client.js +1828 -0
- package/cordis.patch.yml +53 -0
- package/icon.svg +30 -0
- package/lib/host-config-DBCiZOBW.js +900 -0
- package/lib/index.js +3181 -0
- package/lib/llm-adapter-CpcQ3gC5.js +557 -0
- package/locale/en.json +5 -0
- package/locale/zh.json +6 -0
- package/package.json +107 -0
- package/screenshots.json +4 -0
package/client.js
ADDED
|
@@ -0,0 +1,1828 @@
|
|
|
1
|
+
var dsh_connect_modelscope_token_plan_client = (function() {
|
|
2
|
+
|
|
3
|
+
//#region \0rolldown/runtime.js
|
|
4
|
+
var __esmMin = (fn, res, err) => () => {
|
|
5
|
+
if (err) throw err[0];
|
|
6
|
+
try {
|
|
7
|
+
return fn && (res = fn(fn = 0)), res;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
throw err = [e], e;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/client/const.ts
|
|
16
|
+
function modelscopeModelUrl(id) {
|
|
17
|
+
return `${MODELSCOPE_MODEL_URL_BASE}/${encodeURI(id)}`;
|
|
18
|
+
}
|
|
19
|
+
/** 从 `owner/model` 取 owner(无斜杠时整段作 owner)。 */
|
|
20
|
+
function catalogOwner(id) {
|
|
21
|
+
const slash = id.indexOf("/");
|
|
22
|
+
return slash >= 0 ? id.slice(0, slash) : id;
|
|
23
|
+
}
|
|
24
|
+
var NS, PANEL_ID, SNAPSHOT_PATH, MODELS_PATH, TOKEN_PATH, TOKEN_FORGET_PATH, PROBE_PATH, PROVIDER_PATH, PROVIDER_ROSTER_PATH, PROVIDER_RESET_PATH, HIDE_ALL_MODELS, MODELSCOPE_TOKEN_URL, MODELSCOPE_USAGE_URL, MODELSCOPE_MODEL_URL_BASE, FEATURED_OWNERS;
|
|
25
|
+
var init_const = __esmMin((() => {
|
|
26
|
+
NS = "dsh-connect-modelscope-token-plan";
|
|
27
|
+
PANEL_ID = NS;
|
|
28
|
+
SNAPSHOT_PATH = `/api/${NS}/snapshot`;
|
|
29
|
+
MODELS_PATH = `/api/${NS}/models`;
|
|
30
|
+
TOKEN_PATH = `/api/${NS}/token`;
|
|
31
|
+
TOKEN_FORGET_PATH = `/api/${NS}/token/forget`;
|
|
32
|
+
PROBE_PATH = `/api/${NS}/probe`;
|
|
33
|
+
PROVIDER_PATH = `/api/${NS}/provider`;
|
|
34
|
+
PROVIDER_ROSTER_PATH = `/api/${NS}/provider/roster`;
|
|
35
|
+
PROVIDER_RESET_PATH = `/api/${NS}/provider/reset`;
|
|
36
|
+
HIDE_ALL_MODELS = "__hide_all__";
|
|
37
|
+
MODELSCOPE_TOKEN_URL = "https://modelscope.cn/my/myaccesstoken";
|
|
38
|
+
MODELSCOPE_USAGE_URL = "https://modelscope.cn/magicube/usage?tab=consume";
|
|
39
|
+
MODELSCOPE_MODEL_URL_BASE = "https://www.modelscope.cn/models";
|
|
40
|
+
FEATURED_OWNERS = Object.freeze([
|
|
41
|
+
"deepseek-ai",
|
|
42
|
+
"ZhipuAI",
|
|
43
|
+
"Qwen"
|
|
44
|
+
]);
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/client/i18n.ts
|
|
49
|
+
var zh, en;
|
|
50
|
+
var init_i18n = __esmMin((() => {
|
|
51
|
+
zh = {
|
|
52
|
+
"panel.title": "魔搭接入 · 额度面板",
|
|
53
|
+
"panel.refresh": "刷新",
|
|
54
|
+
"panel.updated": "更新于 {time}",
|
|
55
|
+
"panel.loading": "加载中…",
|
|
56
|
+
"panel.error": "读取失败:{error}",
|
|
57
|
+
"panel.configError": "插件配置有误:{error}",
|
|
58
|
+
"panel.networkError": "无法连接本机 Host,通常是临时故障,下一轮自动刷新即可恢复。",
|
|
59
|
+
"panel.timeout": "请求超时,下一轮自动刷新即可恢复。",
|
|
60
|
+
"panel.upstream": "魔搭暂时无法读取,通常下一次自动刷新即可恢复。",
|
|
61
|
+
"panel.internalError": "插件内部错误,不是令牌或配置问题;请查看 DSH 日志中的插件名。",
|
|
62
|
+
"panel.authError": "令牌被拒绝(401/403)。请到「接入」tab 更换访问令牌。",
|
|
63
|
+
"panel.noToken": "还没有配置魔搭访问令牌。到「接入」tab 粘贴一枚 ms-… 令牌即可。",
|
|
64
|
+
"panel.shapeDrift": "上游返回的结构可能有变:{detail}",
|
|
65
|
+
"tab.quota": "额度",
|
|
66
|
+
"tab.models": "模型",
|
|
67
|
+
"tab.access": "接入",
|
|
68
|
+
"section.balance": "魔粒余额(官方数据)",
|
|
69
|
+
"section.local": "本地调用(仅经本插件的调用)",
|
|
70
|
+
"section.trend": "近 {days} 天本地调用趋势",
|
|
71
|
+
"section.events": "限流与错误事件",
|
|
72
|
+
"section.token": "访问令牌",
|
|
73
|
+
"section.expand": "展开",
|
|
74
|
+
"section.collapse": "折叠",
|
|
75
|
+
"balance.available": "可用魔粒",
|
|
76
|
+
"balance.total": "总额",
|
|
77
|
+
"balance.frozen": "冻结",
|
|
78
|
+
"balance.fetched": "读取于 {time}",
|
|
79
|
+
"balance.note": "魔粒是魔搭 API-Inference 的官方额度单位,此数字来自官方接口(openapi/v1/magicubes/balance),是真实余额。预扣 = 进行中任务未返回结果时的暂扣额度。",
|
|
80
|
+
"balance.usagePage": "官方用量明细(网页)→",
|
|
81
|
+
"balance.unavailable": "魔粒余额暂不可读:{error}",
|
|
82
|
+
"quota.headline": "今日 {calls} 次 · {models} 个模型",
|
|
83
|
+
"quota.note": "本地口径:只统计经本插件的调用,直连魔搭的其它客户端不计入;消耗总量以官方魔粒余额为准。",
|
|
84
|
+
"models.none": "目录暂不可读:{error}",
|
|
85
|
+
"models.loading": "读取目录中…",
|
|
86
|
+
"probe.validity": "验令牌(零额度)",
|
|
87
|
+
"probe.validOk": "令牌有效(HTTP {status})",
|
|
88
|
+
"probe.fail": "失败:{error}",
|
|
89
|
+
"models.viewOnSite": "在魔搭查看 →",
|
|
90
|
+
"models.viewOnSiteTitle": "打开模型详情页(魔粒单价等只在网页展示)",
|
|
91
|
+
"models.featuredTitle": "推荐系列(深度求索 / 智谱 / 通义千问)",
|
|
92
|
+
"provider.featured": "推荐",
|
|
93
|
+
"section.provider": "接入为 DSH 模型",
|
|
94
|
+
"provider.enable": "把魔搭模型接入 DSH 模型选择器",
|
|
95
|
+
"provider.on": "已接入",
|
|
96
|
+
"provider.off": "未接入",
|
|
97
|
+
"provider.registered": "已注册(模型可在选择器里选用)",
|
|
98
|
+
"provider.notRegistered": "未注册",
|
|
99
|
+
"provider.llmMissing": "本机 Host 未提供 LLM 注册服务",
|
|
100
|
+
"provider.error": "接入失败:{error}",
|
|
101
|
+
"provider.models": "启用哪些模型",
|
|
102
|
+
"provider.allowAll": "全部",
|
|
103
|
+
"provider.hideAll": "全部隐藏",
|
|
104
|
+
"provider.saveList": "保存清单",
|
|
105
|
+
"provider.reset": "回到默认",
|
|
106
|
+
"provider.enabledCount": "已启用 {count} / {total}",
|
|
107
|
+
"provider.rosterHint": "勾选要进入 DSH 模型选择器的模型;不勾 = 全部提供",
|
|
108
|
+
"provider.quotaExhausted": "额度耗尽",
|
|
109
|
+
"provider.vision": "视觉",
|
|
110
|
+
"provider.notConfigured": "还没有访问令牌,先到「接入」tab 配置",
|
|
111
|
+
"provider.source.panel": "面板",
|
|
112
|
+
"provider.source.config": "配置",
|
|
113
|
+
"trend.none": "还没有本地调用记录。",
|
|
114
|
+
"trend.legend": "柱长相对区间内最大值,仅本地口径。",
|
|
115
|
+
"events.none": "暂无事件。",
|
|
116
|
+
"events.quota": "额度",
|
|
117
|
+
"events.rate_limit": "限频",
|
|
118
|
+
"events.error": "错误",
|
|
119
|
+
"token.status": "状态:{present}(来源 {source},校验 {valid})",
|
|
120
|
+
"token.save": "保存",
|
|
121
|
+
"token.forget": "忘掉已保存",
|
|
122
|
+
"token.placeholder": "粘贴 ms-… 访问令牌",
|
|
123
|
+
"token.hint": "在魔搭「访问令牌」页生成;面板默认读取凭据服务中的 MODELSCOPE_API_KEY,环境变量为回退。",
|
|
124
|
+
"token.ephemeral": "此 Host 没有凭据服务:面板保存的令牌重启即丢,请改用凭据服务或环境变量。",
|
|
125
|
+
"token.link": "打开魔搭访问令牌页 →",
|
|
126
|
+
"source.credentials": "凭据服务",
|
|
127
|
+
"source.env": "环境变量",
|
|
128
|
+
"source.memory": "内存",
|
|
129
|
+
"source.none": "无",
|
|
130
|
+
"present.yes": "已配置",
|
|
131
|
+
"present.no": "未配置",
|
|
132
|
+
"validity.yes": "有效",
|
|
133
|
+
"validity.no": "未校验"
|
|
134
|
+
};
|
|
135
|
+
en = {
|
|
136
|
+
"panel.title": "ModelScope Access · Quota",
|
|
137
|
+
"panel.refresh": "Refresh",
|
|
138
|
+
"panel.updated": "Updated {time}",
|
|
139
|
+
"panel.loading": "Loading…",
|
|
140
|
+
"panel.error": "Read failed: {error}",
|
|
141
|
+
"panel.configError": "Plugin misconfiguration: {error}",
|
|
142
|
+
"panel.networkError": "Cannot reach the local Host — usually transient, the next auto-refresh will recover.",
|
|
143
|
+
"panel.timeout": "Request timed out; the next auto-refresh will recover.",
|
|
144
|
+
"panel.upstream": "ModelScope is temporarily unreadable; the next auto-refresh usually recovers.",
|
|
145
|
+
"panel.internalError": "Internal plugin error — not a token or configuration problem. Check the DSH log for the plugin name.",
|
|
146
|
+
"panel.authError": "Token rejected (401/403). Replace the access token in the Access tab.",
|
|
147
|
+
"panel.noToken": "No ModelScope access token configured yet. Paste an ms-… token in the Access tab.",
|
|
148
|
+
"panel.shapeDrift": "Upstream shape may have changed: {detail}",
|
|
149
|
+
"tab.quota": "Quota",
|
|
150
|
+
"tab.models": "Models",
|
|
151
|
+
"tab.access": "Access",
|
|
152
|
+
"section.balance": "Magicube balance (official)",
|
|
153
|
+
"section.local": "Local calls (only through this plugin)",
|
|
154
|
+
"section.trend": "Local call trend, last {days} days",
|
|
155
|
+
"section.events": "Rate-limit and error events",
|
|
156
|
+
"section.token": "Access token",
|
|
157
|
+
"section.expand": "Expand",
|
|
158
|
+
"section.collapse": "Collapse",
|
|
159
|
+
"balance.available": "Available Magicube",
|
|
160
|
+
"balance.total": "Total",
|
|
161
|
+
"balance.frozen": "Frozen",
|
|
162
|
+
"balance.fetched": "read at {time}",
|
|
163
|
+
"balance.note": "Magicube is the official unit of ModelScope API-Inference quota; this number comes from the official endpoint (openapi/v1/magicubes/balance) and is the real balance. Frozen = held for in-flight tasks.",
|
|
164
|
+
"balance.usagePage": "Official usage details (web) →",
|
|
165
|
+
"balance.unavailable": "Magicube balance temporarily unreadable: {error}",
|
|
166
|
+
"quota.headline": "Today {calls} calls · {models} models",
|
|
167
|
+
"quota.note": "Local scope: only calls through this plugin are counted; clients calling ModelScope directly are not. Total consumption is governed by the official Magicube balance.",
|
|
168
|
+
"models.none": "Catalog unavailable: {error}",
|
|
169
|
+
"models.loading": "Loading catalog…",
|
|
170
|
+
"probe.validity": "Verify token (free)",
|
|
171
|
+
"probe.validOk": "Token valid (HTTP {status})",
|
|
172
|
+
"probe.fail": "Failed: {error}",
|
|
173
|
+
"models.viewOnSite": "View on ModelScope →",
|
|
174
|
+
"models.viewOnSiteTitle": "Open the model page (per-model Magicube price is web-only)",
|
|
175
|
+
"models.featuredTitle": "Recommended family (DeepSeek / Zhipu / Qwen)",
|
|
176
|
+
"provider.featured": "Recommended",
|
|
177
|
+
"section.provider": "Register as a DSH model",
|
|
178
|
+
"provider.enable": "Register ModelScope models in the DSH model picker",
|
|
179
|
+
"provider.on": "On",
|
|
180
|
+
"provider.off": "Off",
|
|
181
|
+
"provider.registered": "Registered (models selectable in the picker)",
|
|
182
|
+
"provider.notRegistered": "Not registered",
|
|
183
|
+
"provider.llmMissing": "This Host exposes no LLM service",
|
|
184
|
+
"provider.error": "Provider error: {error}",
|
|
185
|
+
"provider.models": "Which models to enable",
|
|
186
|
+
"provider.allowAll": "All",
|
|
187
|
+
"provider.hideAll": "Hide all",
|
|
188
|
+
"provider.saveList": "Save list",
|
|
189
|
+
"provider.reset": "Reset",
|
|
190
|
+
"provider.enabledCount": "{count} / {total} enabled",
|
|
191
|
+
"provider.rosterHint": "Tick the models to offer; unticked = offer all",
|
|
192
|
+
"provider.quotaExhausted": "Quota exhausted",
|
|
193
|
+
"provider.vision": "vision",
|
|
194
|
+
"provider.notConfigured": "No token yet — configure it in the Access tab",
|
|
195
|
+
"provider.source.panel": "panel",
|
|
196
|
+
"provider.source.config": "config",
|
|
197
|
+
"trend.none": "No local call records yet.",
|
|
198
|
+
"trend.legend": "Bar lengths are relative to the maximum in the range (local scope only).",
|
|
199
|
+
"events.none": "No events.",
|
|
200
|
+
"events.quota": "Quota",
|
|
201
|
+
"events.rate_limit": "Rate limit",
|
|
202
|
+
"events.error": "Error",
|
|
203
|
+
"token.status": "Status: {present} (source {source}, check {valid})",
|
|
204
|
+
"token.save": "Save",
|
|
205
|
+
"token.forget": "Forget saved",
|
|
206
|
+
"token.placeholder": "Paste an ms-… access token",
|
|
207
|
+
"token.hint": "Generate one on the ModelScope access-token page; the panel reads MODELSCOPE_API_KEY from the credentials service first, environment as fallback.",
|
|
208
|
+
"token.ephemeral": "This Host has no credentials service: a token saved in the panel is lost on restart. Use the credentials service or an environment variable.",
|
|
209
|
+
"token.link": "Open the ModelScope access-token page →",
|
|
210
|
+
"source.credentials": "credentials",
|
|
211
|
+
"source.env": "environment",
|
|
212
|
+
"source.memory": "memory",
|
|
213
|
+
"source.none": "none",
|
|
214
|
+
"present.yes": "configured",
|
|
215
|
+
"present.no": "not configured",
|
|
216
|
+
"validity.yes": "verified",
|
|
217
|
+
"validity.no": "unverified"
|
|
218
|
+
};
|
|
219
|
+
}));
|
|
220
|
+
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/client/format.ts
|
|
223
|
+
/** Time and number formatters(与姊妹插件 format.ts 受控复制,按需裁剪)。 */
|
|
224
|
+
/**
|
|
225
|
+
* Host 声明的 cadence(秒)转毫秒;无可用数字时回退。Host 在源头已 clamp,
|
|
226
|
+
* 面板不做第二次 opinion;亚秒值向上取整到 1s(setInterval 的 0 = 尽快)。
|
|
227
|
+
*/
|
|
228
|
+
function statedCadenceMs(seconds, fallbackMs) {
|
|
229
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) return fallbackMs;
|
|
230
|
+
return Math.max(1, Math.floor(seconds)) * 1e3;
|
|
231
|
+
}
|
|
232
|
+
/** `MM-DD HH:mm`(事件时间戳用)。 */
|
|
233
|
+
function clockLong(epoch) {
|
|
234
|
+
if (typeof epoch !== "number" || !Number.isFinite(epoch) || epoch <= 0) return "—";
|
|
235
|
+
const date = /* @__PURE__ */ new Date(epoch * 1e3);
|
|
236
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
237
|
+
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
238
|
+
}
|
|
239
|
+
/** ISO 字符串 → `MM-DD HH:mm`;非法输入返回 "—"。 */
|
|
240
|
+
function isoTime(iso) {
|
|
241
|
+
if (typeof iso !== "string" || iso === "") return "—";
|
|
242
|
+
const epoch = Date.parse(iso);
|
|
243
|
+
return Number.isNaN(epoch) ? "—" : clockLong(epoch / 1e3);
|
|
244
|
+
}
|
|
245
|
+
/** 数字文本:≥10000 整数千分位,否则最多两位小数。 */
|
|
246
|
+
function count(value) {
|
|
247
|
+
const number = typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
248
|
+
if (number >= 1e4) return Math.round(number).toLocaleString();
|
|
249
|
+
return String(Math.round(number * 100) / 100);
|
|
250
|
+
}
|
|
251
|
+
/** 填 `{token}` 模板。 */
|
|
252
|
+
function format(template, vars) {
|
|
253
|
+
let text = template;
|
|
254
|
+
for (const [key, value] of Object.entries(vars || {})) text = text.split(`{${key}}`).join(String(value));
|
|
255
|
+
return text;
|
|
256
|
+
}
|
|
257
|
+
/** catch 到的值 → 面板唯一错误行;绝不 `[object Object]`。 */
|
|
258
|
+
function errorText(why) {
|
|
259
|
+
if (why instanceof Error) return why.message;
|
|
260
|
+
if (typeof why === "string") return why;
|
|
261
|
+
if (why === null || why === void 0) return String(why);
|
|
262
|
+
try {
|
|
263
|
+
return JSON.stringify(why) ?? String(why);
|
|
264
|
+
} catch {
|
|
265
|
+
return String(why);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
var init_format = __esmMin((() => {}));
|
|
269
|
+
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/client/runtime.ts
|
|
272
|
+
/** Hand the loader-provided React to the rest of the client. One-shot. */
|
|
273
|
+
function provideClientReact(value) {
|
|
274
|
+
if (typeof value !== "object" || value === null) throw new Error("client: the loader did not hand over a react module");
|
|
275
|
+
api = value;
|
|
276
|
+
}
|
|
277
|
+
function reactApi() {
|
|
278
|
+
if (api === null) throw new Error("client: react used before clientFactory ran");
|
|
279
|
+
return api;
|
|
280
|
+
}
|
|
281
|
+
var api, h, useState, useEffect, useCallback, useMemo, useRef;
|
|
282
|
+
var init_runtime = __esmMin((() => {
|
|
283
|
+
api = null;
|
|
284
|
+
h = (type, props, ...children) => reactApi().createElement(type, props, ...children);
|
|
285
|
+
useState = (initial) => reactApi().useState(initial);
|
|
286
|
+
useEffect = (effect, deps) => reactApi().useEffect(effect, deps);
|
|
287
|
+
useCallback = (callback, deps) => reactApi().useCallback(callback, deps);
|
|
288
|
+
useMemo = (factory, deps) => reactApi().useMemo(factory, deps);
|
|
289
|
+
useRef = (initial) => reactApi().useRef(initial);
|
|
290
|
+
}));
|
|
291
|
+
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/client/styles.ts
|
|
294
|
+
var BRAND, BUTTON, S;
|
|
295
|
+
var init_styles = __esmMin((() => {
|
|
296
|
+
BRAND = "var(--modelscope-brand, #7B3FF2)";
|
|
297
|
+
BUTTON = {
|
|
298
|
+
height: 30,
|
|
299
|
+
padding: "0 12px",
|
|
300
|
+
borderRadius: 8,
|
|
301
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
302
|
+
background: "var(--dsw-alias-bg-layer-2)",
|
|
303
|
+
color: "var(--dsw-alias-label-primary)",
|
|
304
|
+
fontSize: 13,
|
|
305
|
+
cursor: "pointer"
|
|
306
|
+
};
|
|
307
|
+
S = {
|
|
308
|
+
page: {
|
|
309
|
+
flex: "1 1 auto",
|
|
310
|
+
height: "100%",
|
|
311
|
+
minHeight: 0,
|
|
312
|
+
display: "flex",
|
|
313
|
+
flexDirection: "column",
|
|
314
|
+
overflow: "hidden",
|
|
315
|
+
color: "var(--dsw-alias-label-primary)",
|
|
316
|
+
fontSize: 14,
|
|
317
|
+
lineHeight: "22px"
|
|
318
|
+
},
|
|
319
|
+
headerBar: {
|
|
320
|
+
flex: "none",
|
|
321
|
+
background: "var(--dsw-alias-bg-base)",
|
|
322
|
+
position: "relative",
|
|
323
|
+
zIndex: 1
|
|
324
|
+
},
|
|
325
|
+
header: {
|
|
326
|
+
display: "flex",
|
|
327
|
+
alignItems: "center",
|
|
328
|
+
gap: 12,
|
|
329
|
+
padding: "16px 0 12px"
|
|
330
|
+
},
|
|
331
|
+
scroll: {
|
|
332
|
+
flex: 1,
|
|
333
|
+
minHeight: 0,
|
|
334
|
+
overflowY: "auto",
|
|
335
|
+
overflowX: "hidden"
|
|
336
|
+
},
|
|
337
|
+
content: { padding: "6px 0 56px" },
|
|
338
|
+
tabBar: {
|
|
339
|
+
display: "flex",
|
|
340
|
+
gap: 4,
|
|
341
|
+
borderBottom: "1px solid var(--dsw-alias-border-l1)",
|
|
342
|
+
marginBottom: 4
|
|
343
|
+
},
|
|
344
|
+
tab: {
|
|
345
|
+
appearance: "none",
|
|
346
|
+
background: "none",
|
|
347
|
+
border: "none",
|
|
348
|
+
borderBottom: "2px solid transparent",
|
|
349
|
+
padding: "8px 12px",
|
|
350
|
+
fontSize: 13,
|
|
351
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
352
|
+
cursor: "pointer",
|
|
353
|
+
outline: "none"
|
|
354
|
+
},
|
|
355
|
+
tabActive: {
|
|
356
|
+
color: "var(--dsw-alias-label-primary)",
|
|
357
|
+
fontWeight: 600,
|
|
358
|
+
borderBottom: `2px solid ${BRAND}`
|
|
359
|
+
},
|
|
360
|
+
title: {
|
|
361
|
+
margin: 0,
|
|
362
|
+
fontSize: 20,
|
|
363
|
+
fontWeight: 600,
|
|
364
|
+
lineHeight: "28px"
|
|
365
|
+
},
|
|
366
|
+
updated: {
|
|
367
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
368
|
+
fontSize: 12
|
|
369
|
+
},
|
|
370
|
+
spacer: { flex: 1 },
|
|
371
|
+
cluster: {
|
|
372
|
+
display: "inline-flex",
|
|
373
|
+
alignItems: "center",
|
|
374
|
+
gap: 12,
|
|
375
|
+
flexWrap: "wrap",
|
|
376
|
+
justifyContent: "flex-end"
|
|
377
|
+
},
|
|
378
|
+
button: BUTTON,
|
|
379
|
+
sectionTitle: {
|
|
380
|
+
margin: "22px 0 10px",
|
|
381
|
+
fontSize: 13,
|
|
382
|
+
fontWeight: 600,
|
|
383
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
384
|
+
},
|
|
385
|
+
sectionCard: {
|
|
386
|
+
border: "1px solid var(--dsw-alias-border-l1)",
|
|
387
|
+
borderRadius: 12,
|
|
388
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
389
|
+
overflow: "hidden",
|
|
390
|
+
marginTop: 22
|
|
391
|
+
},
|
|
392
|
+
sectionHead: {
|
|
393
|
+
display: "flex",
|
|
394
|
+
alignItems: "center",
|
|
395
|
+
gap: 12,
|
|
396
|
+
width: "100%",
|
|
397
|
+
padding: "12px 16px",
|
|
398
|
+
background: "none",
|
|
399
|
+
border: "none",
|
|
400
|
+
cursor: "pointer",
|
|
401
|
+
textAlign: "left"
|
|
402
|
+
},
|
|
403
|
+
sectionHeadTitle: {
|
|
404
|
+
flex: 1,
|
|
405
|
+
minWidth: 0,
|
|
406
|
+
fontSize: 15,
|
|
407
|
+
fontWeight: 600,
|
|
408
|
+
color: "var(--dsw-alias-label-primary)"
|
|
409
|
+
},
|
|
410
|
+
chevron: {
|
|
411
|
+
display: "inline-flex",
|
|
412
|
+
flex: "none",
|
|
413
|
+
transition: "transform 0.15s ease",
|
|
414
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
415
|
+
},
|
|
416
|
+
chevronOpen: { transform: "rotate(180deg)" },
|
|
417
|
+
sectionBody: {
|
|
418
|
+
borderTop: "1px solid var(--dsw-alias-border-l1)",
|
|
419
|
+
margin: "0 16px",
|
|
420
|
+
padding: "12px 0 16px"
|
|
421
|
+
},
|
|
422
|
+
card: {
|
|
423
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
424
|
+
border: "1px solid var(--dsw-alias-border-l1)",
|
|
425
|
+
borderRadius: 12,
|
|
426
|
+
padding: 16
|
|
427
|
+
},
|
|
428
|
+
poolName: {
|
|
429
|
+
fontSize: 15,
|
|
430
|
+
fontWeight: 600
|
|
431
|
+
},
|
|
432
|
+
quotaLabel: {
|
|
433
|
+
fontSize: 12,
|
|
434
|
+
fontWeight: 500,
|
|
435
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
436
|
+
},
|
|
437
|
+
statGrid: {
|
|
438
|
+
display: "grid",
|
|
439
|
+
gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 110px), 1fr))",
|
|
440
|
+
gap: 10,
|
|
441
|
+
marginTop: 12
|
|
442
|
+
},
|
|
443
|
+
statCard: {
|
|
444
|
+
display: "flex",
|
|
445
|
+
flexDirection: "column",
|
|
446
|
+
gap: 4,
|
|
447
|
+
minWidth: 0,
|
|
448
|
+
padding: "10px 12px",
|
|
449
|
+
borderRadius: 10,
|
|
450
|
+
background: "var(--dsw-alias-bg-layer-2)"
|
|
451
|
+
},
|
|
452
|
+
statValue: {
|
|
453
|
+
fontSize: 16,
|
|
454
|
+
fontWeight: 600,
|
|
455
|
+
lineHeight: "20px",
|
|
456
|
+
fontVariantNumeric: "tabular-nums"
|
|
457
|
+
},
|
|
458
|
+
quotaUsed: {
|
|
459
|
+
fontSize: 11,
|
|
460
|
+
lineHeight: "15px",
|
|
461
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
462
|
+
fontVariantNumeric: "tabular-nums"
|
|
463
|
+
},
|
|
464
|
+
barFill: {
|
|
465
|
+
height: "100%",
|
|
466
|
+
borderRadius: 3,
|
|
467
|
+
background: BRAND
|
|
468
|
+
},
|
|
469
|
+
modelTag: {
|
|
470
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
471
|
+
fontSize: 11,
|
|
472
|
+
padding: "2px 6px",
|
|
473
|
+
borderRadius: 6,
|
|
474
|
+
background: "var(--dsw-alias-bg-layer-2)",
|
|
475
|
+
border: "1px solid var(--dsw-alias-border-l1)"
|
|
476
|
+
},
|
|
477
|
+
trendRow: {
|
|
478
|
+
display: "flex",
|
|
479
|
+
flexDirection: "column",
|
|
480
|
+
gap: 8,
|
|
481
|
+
padding: "10px 0",
|
|
482
|
+
borderBottom: "1px solid var(--dsw-alias-border-l1)"
|
|
483
|
+
},
|
|
484
|
+
trendRowHead: {
|
|
485
|
+
display: "flex",
|
|
486
|
+
alignItems: "baseline",
|
|
487
|
+
justifyContent: "space-between",
|
|
488
|
+
gap: 12,
|
|
489
|
+
minWidth: 0
|
|
490
|
+
},
|
|
491
|
+
trendModel: {
|
|
492
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
493
|
+
fontSize: 12,
|
|
494
|
+
minWidth: 0,
|
|
495
|
+
overflow: "hidden",
|
|
496
|
+
textOverflow: "ellipsis",
|
|
497
|
+
whiteSpace: "nowrap"
|
|
498
|
+
},
|
|
499
|
+
trendCredits: {
|
|
500
|
+
fontSize: 13,
|
|
501
|
+
fontWeight: 600,
|
|
502
|
+
fontVariantNumeric: "tabular-nums"
|
|
503
|
+
},
|
|
504
|
+
trendBar: {
|
|
505
|
+
height: 6,
|
|
506
|
+
borderRadius: 3,
|
|
507
|
+
background: "var(--dsw-alias-bg-layer-2)",
|
|
508
|
+
overflow: "hidden"
|
|
509
|
+
},
|
|
510
|
+
trendLegend: {
|
|
511
|
+
marginTop: 10,
|
|
512
|
+
fontSize: 11,
|
|
513
|
+
lineHeight: "16px",
|
|
514
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
515
|
+
},
|
|
516
|
+
muted: { color: "var(--dsw-alias-label-secondary)" },
|
|
517
|
+
error: { color: "var(--dsw-alias-state-error-primary)" },
|
|
518
|
+
empty: {
|
|
519
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
520
|
+
padding: "18px 0"
|
|
521
|
+
},
|
|
522
|
+
input: {
|
|
523
|
+
height: 32,
|
|
524
|
+
padding: "0 10px",
|
|
525
|
+
borderRadius: 8,
|
|
526
|
+
fontSize: 13,
|
|
527
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
528
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
529
|
+
color: "var(--dsw-alias-label-primary)"
|
|
530
|
+
},
|
|
531
|
+
/** Real shell tokens — replaces the color-mix hack that faked "on-primary". */
|
|
532
|
+
primary: {
|
|
533
|
+
height: 32,
|
|
534
|
+
padding: "0 16px",
|
|
535
|
+
borderRadius: 8,
|
|
536
|
+
fontSize: 13,
|
|
537
|
+
fontWeight: 500,
|
|
538
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
539
|
+
background: "var(--dsw-alias-button-primary-fill)",
|
|
540
|
+
color: "var(--dsw-alias-label-primary-foreground)",
|
|
541
|
+
cursor: "pointer"
|
|
542
|
+
},
|
|
543
|
+
formError: {
|
|
544
|
+
color: "var(--dsw-alias-state-error-primary)",
|
|
545
|
+
fontSize: 12,
|
|
546
|
+
margin: "10px 0 0"
|
|
547
|
+
},
|
|
548
|
+
formNote: {
|
|
549
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
550
|
+
fontSize: 12,
|
|
551
|
+
margin: "10px 0 0"
|
|
552
|
+
},
|
|
553
|
+
rosterTools: {
|
|
554
|
+
display: "flex",
|
|
555
|
+
gap: 8,
|
|
556
|
+
alignItems: "center",
|
|
557
|
+
flexWrap: "wrap",
|
|
558
|
+
marginBottom: 10
|
|
559
|
+
},
|
|
560
|
+
rosterCount: {
|
|
561
|
+
fontSize: 12,
|
|
562
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
563
|
+
fontVariantNumeric: "tabular-nums",
|
|
564
|
+
marginLeft: "auto"
|
|
565
|
+
},
|
|
566
|
+
rosterBulk: {
|
|
567
|
+
...BUTTON,
|
|
568
|
+
height: 32
|
|
569
|
+
},
|
|
570
|
+
modelList: {
|
|
571
|
+
display: "flex",
|
|
572
|
+
flexDirection: "column",
|
|
573
|
+
margin: 0,
|
|
574
|
+
padding: 0,
|
|
575
|
+
listStyle: "none"
|
|
576
|
+
},
|
|
577
|
+
modelRow: {
|
|
578
|
+
display: "flex",
|
|
579
|
+
flexDirection: "column",
|
|
580
|
+
gap: 2,
|
|
581
|
+
padding: "8px 4px",
|
|
582
|
+
borderBottom: "1px solid var(--dsw-alias-border-l1)"
|
|
583
|
+
},
|
|
584
|
+
modelRowHead: {
|
|
585
|
+
display: "flex",
|
|
586
|
+
alignItems: "center",
|
|
587
|
+
gap: 8
|
|
588
|
+
},
|
|
589
|
+
modelRowOff: { opacity: .55 },
|
|
590
|
+
modelCheck: {
|
|
591
|
+
flex: "none",
|
|
592
|
+
width: 15,
|
|
593
|
+
height: 15,
|
|
594
|
+
cursor: "pointer",
|
|
595
|
+
accentColor: BRAND,
|
|
596
|
+
margin: 0
|
|
597
|
+
},
|
|
598
|
+
modelName: {
|
|
599
|
+
flex: "0 1 auto",
|
|
600
|
+
minWidth: 0,
|
|
601
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
602
|
+
fontSize: 12,
|
|
603
|
+
overflow: "hidden",
|
|
604
|
+
textOverflow: "ellipsis",
|
|
605
|
+
whiteSpace: "nowrap"
|
|
606
|
+
},
|
|
607
|
+
modelBadge: {
|
|
608
|
+
flex: "none",
|
|
609
|
+
fontSize: 11,
|
|
610
|
+
padding: "1px 7px",
|
|
611
|
+
borderRadius: 999,
|
|
612
|
+
background: "var(--dsw-alias-bg-layer-2)",
|
|
613
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
614
|
+
},
|
|
615
|
+
modelBadgeFeatured: {
|
|
616
|
+
flex: "none",
|
|
617
|
+
fontSize: 11,
|
|
618
|
+
padding: "1px 7px",
|
|
619
|
+
borderRadius: 999,
|
|
620
|
+
background: "var(--dsw-alias-bg-layer-2)",
|
|
621
|
+
color: BRAND,
|
|
622
|
+
fontWeight: 600
|
|
623
|
+
},
|
|
624
|
+
rosterFoot: {
|
|
625
|
+
display: "flex",
|
|
626
|
+
gap: 8,
|
|
627
|
+
alignItems: "center",
|
|
628
|
+
marginTop: 10
|
|
629
|
+
},
|
|
630
|
+
modelPanel: {
|
|
631
|
+
border: "1px solid var(--dsw-alias-border-l1)",
|
|
632
|
+
borderRadius: 12,
|
|
633
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
634
|
+
padding: "12px 14px"
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
}));
|
|
638
|
+
|
|
639
|
+
//#endregion
|
|
640
|
+
//#region src/shared/wire.ts
|
|
641
|
+
var SNAPSHOT_REQUIRED_KEYS;
|
|
642
|
+
var init_wire = __esmMin((() => {
|
|
643
|
+
SNAPSHOT_REQUIRED_KEYS = Object.freeze([
|
|
644
|
+
"ok",
|
|
645
|
+
"name",
|
|
646
|
+
"version",
|
|
647
|
+
"now",
|
|
648
|
+
"pollSeconds",
|
|
649
|
+
"cacheSeconds",
|
|
650
|
+
"token",
|
|
651
|
+
"balance",
|
|
652
|
+
"quota",
|
|
653
|
+
"events",
|
|
654
|
+
"trend",
|
|
655
|
+
"models",
|
|
656
|
+
"provider",
|
|
657
|
+
"shapeWarnings",
|
|
658
|
+
"quotaError"
|
|
659
|
+
]);
|
|
660
|
+
}));
|
|
661
|
+
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region src/client/snapshot.ts
|
|
664
|
+
/**
|
|
665
|
+
* 缺失块的空形状,**逐字对齐 Host 的降级实现**(`snapshot-aggregate.ts` 的
|
|
666
|
+
* `providerDegraded` 与各 `soft()` 失败分支、TokenStatus 的失败分支)。
|
|
667
|
+
*
|
|
668
|
+
* 这里的每一条都是「Host 那一路失败时真的产出的形状」,不是随手捏的:面板渲染
|
|
669
|
+
* 的每一个字段都必须有一个与 Host 同构的空值,否则补出来的空块自己就会炸——
|
|
670
|
+
* 那正是本函数存在的原因。
|
|
671
|
+
*/
|
|
672
|
+
function emptyBlock(key) {
|
|
673
|
+
switch (key) {
|
|
674
|
+
case "token": return {
|
|
675
|
+
present: false,
|
|
676
|
+
source: "none",
|
|
677
|
+
valid: null,
|
|
678
|
+
checkedAt: null,
|
|
679
|
+
ephemeral: true
|
|
680
|
+
};
|
|
681
|
+
case "balance": return {
|
|
682
|
+
available: null,
|
|
683
|
+
total: null,
|
|
684
|
+
frozen: null,
|
|
685
|
+
fetchedAt: null,
|
|
686
|
+
error: null
|
|
687
|
+
};
|
|
688
|
+
case "quota": return {
|
|
689
|
+
daily: { usedLocal: 0 },
|
|
690
|
+
perModel: [],
|
|
691
|
+
countingNote: "local-counting"
|
|
692
|
+
};
|
|
693
|
+
case "events": return [];
|
|
694
|
+
case "trend": return {
|
|
695
|
+
days: 0,
|
|
696
|
+
buckets: []
|
|
697
|
+
};
|
|
698
|
+
case "models": return {
|
|
699
|
+
available: false,
|
|
700
|
+
count: 0,
|
|
701
|
+
sample: [],
|
|
702
|
+
error: null
|
|
703
|
+
};
|
|
704
|
+
case "provider": return DEGRADED_PROVIDER;
|
|
705
|
+
case "shapeWarnings":
|
|
706
|
+
case "quotaError": return key === "quotaError" ? null : [];
|
|
707
|
+
default: return;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
/** 把缺失的顶层键补成空形状(不改原body,返回新对象)。 */
|
|
711
|
+
function withEmptyBlocks(raw, missingKeys) {
|
|
712
|
+
const out = { ...raw };
|
|
713
|
+
for (const key of missingKeys) out[key] = emptyBlock(key);
|
|
714
|
+
return out;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* 读一个快照应答。HTTP 恒 200,成败看 body.ok。
|
|
718
|
+
*
|
|
719
|
+
* `ok:true` 的body **必须**带齐 {@link SNAPSHOT_REQUIRED_KEYS} 的每个顶层键,
|
|
720
|
+
* 缺一个都在这里被归一,而不是留到渲染期炸掉。曾经这里是裸 cast,于是
|
|
721
|
+
* `panel-page.ts` 里 `data?.models.sample[0]` 这类解引用会抛 TypeError 把整个面板
|
|
722
|
+
* 炸掉——`data?.` 只护住了 `data` 本身,护不住它下面的 `models`。而那一行在
|
|
723
|
+
* **所有 tab 上都执行**,所以任意一个键缺失都不止炸当前 tab。
|
|
724
|
+
*
|
|
725
|
+
* 缺键走两件事:把确实缺的那个块补成同构的空形状(让面板渲染「空」而不是
|
|
726
|
+
* 崩),并把缺失名单带进 `shapeWarnings` 的等价物——`SnapshotRead` 上的
|
|
727
|
+
* `missingKeys`,由 `viewOf` 呈现给用户。**不**静默:Host 少给键是双端契约
|
|
728
|
+
* 漂移,用户该看见。
|
|
729
|
+
*/
|
|
730
|
+
function interpretSnapshot(body) {
|
|
731
|
+
const payload = body;
|
|
732
|
+
if (payload && payload.ok === false) return {
|
|
733
|
+
data: null,
|
|
734
|
+
error: {
|
|
735
|
+
message: payload.error || "unexpected payload",
|
|
736
|
+
code: payload.code
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
if (!payload || payload.ok !== true) return {
|
|
740
|
+
data: null,
|
|
741
|
+
error: "unexpected payload"
|
|
742
|
+
};
|
|
743
|
+
const raw = payload;
|
|
744
|
+
const missingKeys = SNAPSHOT_REQUIRED_KEYS.filter((key) => raw[key] === void 0);
|
|
745
|
+
return {
|
|
746
|
+
data: missingKeys.length === 0 ? raw : withEmptyBlocks(raw, missingKeys),
|
|
747
|
+
error: null,
|
|
748
|
+
missingKeys
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* 把 wire/路由返回的 provider 块归一成 `ProviderStatus`;不合法一律降级。
|
|
753
|
+
* 形状由 wire.ts 钉死,本函数只做防御性归一(旧 Host 可能缺字段)。
|
|
754
|
+
*/
|
|
755
|
+
function providerOf(raw) {
|
|
756
|
+
if (raw === null || typeof raw !== "object") return DEGRADED_PROVIDER;
|
|
757
|
+
const p = raw;
|
|
758
|
+
const ids = Array.isArray(p.enabledIds) ? p.enabledIds.map((id) => String(id ?? "")).filter((id) => id !== "") : [];
|
|
759
|
+
const roster = Array.isArray(p.roster) ? p.roster.map((entry) => {
|
|
760
|
+
const row = entry ?? {};
|
|
761
|
+
const id = String(row.id ?? "");
|
|
762
|
+
return {
|
|
763
|
+
id,
|
|
764
|
+
name: typeof row.name === "string" && row.name !== "" ? row.name : id,
|
|
765
|
+
vision: row.vision === true,
|
|
766
|
+
available: row.available !== false,
|
|
767
|
+
quotaExhausted: row.quotaExhausted === true
|
|
768
|
+
};
|
|
769
|
+
}).filter((entry) => entry.id !== "") : [];
|
|
770
|
+
return {
|
|
771
|
+
enabled: p.enabled === true,
|
|
772
|
+
source: p.source === "panel" ? "panel" : "config",
|
|
773
|
+
llmAvailable: p.llmAvailable === true,
|
|
774
|
+
registered: p.registered === true,
|
|
775
|
+
error: typeof p.error === "string" ? p.error : null,
|
|
776
|
+
modelCount: typeof p.modelCount === "number" ? p.modelCount : roster.length,
|
|
777
|
+
enabledCount: typeof p.enabledCount === "number" ? p.enabledCount : 0,
|
|
778
|
+
allowed: p.allowed === "none" ? "none" : p.allowed === "list" ? "list" : "all",
|
|
779
|
+
enabledIds: ids,
|
|
780
|
+
roster
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
/** 非 2xx 响应的降级读法(无 body,状态码是唯一线索)。 */
|
|
784
|
+
function errorOfStatus(status) {
|
|
785
|
+
if (status === 401 || status === 403) return {
|
|
786
|
+
message: `HTTP ${status}`,
|
|
787
|
+
code: "auth_error"
|
|
788
|
+
};
|
|
789
|
+
return `HTTP ${status}`;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* 面板决策:这张快照意味着什么。纯函数,Node 套件驱动同一个函数。
|
|
793
|
+
*
|
|
794
|
+
* `missingKeys` 是 `interpretSnapshot` 查出的缺失顶层键(非空 = 双端契约漂移)。
|
|
795
|
+
* 它**不**把面板变成错误态——数据能渲染就渲染——但会作为一条 shapeWarning
|
|
796
|
+
* 冒到面板上,因为「Host 少给了键」是维护者要修的事,用户该看见而不是面对
|
|
797
|
+
* 一个悄悄少了一半信息的界面。
|
|
798
|
+
*/
|
|
799
|
+
function viewOf(data, error, tt, missingKeys = []) {
|
|
800
|
+
const failure = error === null || error === void 0 ? null : typeof error === "string" ? {
|
|
801
|
+
message: error,
|
|
802
|
+
code: null
|
|
803
|
+
} : error;
|
|
804
|
+
const needsSetup = data === null && !FORM_EXCLUDED_CODES.has(failure?.code ?? null);
|
|
805
|
+
const guidanceKey = failure === null ? null : GUIDANCE_BY_CODE[failure.code] ?? null;
|
|
806
|
+
const guidance = guidanceKey === null ? null : guidanceKey === "panel.configError" ? format(tt(guidanceKey), { error: failure?.message }) : tt(guidanceKey);
|
|
807
|
+
const shapeWarnings = Array.isArray(data?.shapeWarnings) ? [...data.shapeWarnings] : [];
|
|
808
|
+
if (missingKeys.length > 0) shapeWarnings.push(`snapshot: Host omitted required key(s): ${missingKeys.join(", ")}`);
|
|
809
|
+
return {
|
|
810
|
+
failure,
|
|
811
|
+
needsSetup,
|
|
812
|
+
guidanceKey,
|
|
813
|
+
guidance,
|
|
814
|
+
shapeWarnings
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
var DEGRADED_PROVIDER, GUIDANCE_BY_CODE, FORM_EXCLUDED_CODES;
|
|
818
|
+
var init_snapshot = __esmMin((() => {
|
|
819
|
+
init_format();
|
|
820
|
+
init_wire();
|
|
821
|
+
DEGRADED_PROVIDER = Object.freeze({
|
|
822
|
+
enabled: false,
|
|
823
|
+
source: "config",
|
|
824
|
+
llmAvailable: false,
|
|
825
|
+
registered: false,
|
|
826
|
+
error: "unavailable",
|
|
827
|
+
modelCount: 0,
|
|
828
|
+
enabledCount: 0,
|
|
829
|
+
allowed: "all",
|
|
830
|
+
enabledIds: [],
|
|
831
|
+
roster: []
|
|
832
|
+
});
|
|
833
|
+
GUIDANCE_BY_CODE = Object.freeze({
|
|
834
|
+
auth_error: "panel.authError",
|
|
835
|
+
config_error: "panel.configError",
|
|
836
|
+
network_error: "panel.networkError",
|
|
837
|
+
timeout_error: "panel.timeout",
|
|
838
|
+
upstream_error: "panel.upstream",
|
|
839
|
+
rate_limited: "panel.upstream",
|
|
840
|
+
quota_exceeded: "panel.upstream",
|
|
841
|
+
internal_error: "panel.internalError"
|
|
842
|
+
});
|
|
843
|
+
FORM_EXCLUDED_CODES = Object.freeze(/* @__PURE__ */ new Set([
|
|
844
|
+
"config_error",
|
|
845
|
+
"network_error",
|
|
846
|
+
"timeout_error",
|
|
847
|
+
"upstream_error",
|
|
848
|
+
"rate_limited",
|
|
849
|
+
"quota_exceeded",
|
|
850
|
+
"internal_error"
|
|
851
|
+
]));
|
|
852
|
+
}));
|
|
853
|
+
|
|
854
|
+
//#endregion
|
|
855
|
+
//#region src/client/toggle-switch.ts
|
|
856
|
+
/**
|
|
857
|
+
* 滑杆开关 + 短标签 + tooltip。
|
|
858
|
+
* @param props - 见 {@link ToggleSwitchProps}。
|
|
859
|
+
* @returns 包着真 checkbox input 的 `<label>` 树。
|
|
860
|
+
*/
|
|
861
|
+
function ToggleSwitch({ checked, onChange, label, busyLabel, busy = false, title }) {
|
|
862
|
+
const on = checked === true;
|
|
863
|
+
const text = busy && busyLabel !== void 0 ? busyLabel : label;
|
|
864
|
+
return h("label", {
|
|
865
|
+
style: {
|
|
866
|
+
display: "inline-flex",
|
|
867
|
+
alignItems: "center",
|
|
868
|
+
gap: 8,
|
|
869
|
+
position: "relative",
|
|
870
|
+
cursor: busy ? "wait" : "pointer",
|
|
871
|
+
opacity: busy ? .55 : 1,
|
|
872
|
+
verticalAlign: "middle"
|
|
873
|
+
},
|
|
874
|
+
...title !== void 0 && title !== "" ? { title } : {}
|
|
875
|
+
}, h("span", { style: {
|
|
876
|
+
position: "relative",
|
|
877
|
+
display: "inline-block",
|
|
878
|
+
width: TRACK_W,
|
|
879
|
+
height: TRACK_H,
|
|
880
|
+
flex: "none"
|
|
881
|
+
} }, h("input", {
|
|
882
|
+
type: "checkbox",
|
|
883
|
+
checked: on,
|
|
884
|
+
disabled: busy,
|
|
885
|
+
onChange,
|
|
886
|
+
style: {
|
|
887
|
+
position: "absolute",
|
|
888
|
+
inset: 0,
|
|
889
|
+
width: TRACK_W,
|
|
890
|
+
height: TRACK_H,
|
|
891
|
+
margin: 0,
|
|
892
|
+
opacity: 0,
|
|
893
|
+
cursor: busy ? "wait" : "pointer"
|
|
894
|
+
}
|
|
895
|
+
}), h("span", {
|
|
896
|
+
"aria-hidden": "true",
|
|
897
|
+
style: {
|
|
898
|
+
position: "absolute",
|
|
899
|
+
inset: 0,
|
|
900
|
+
borderRadius: 999,
|
|
901
|
+
pointerEvents: "none",
|
|
902
|
+
border: `1px solid ${on ? "var(--modelscope-brand, #7B3FF2)" : "var(--dsw-alias-border-l2, #36373b)"}`,
|
|
903
|
+
background: on ? "var(--modelscope-brand, #7B3FF2)" : "var(--dsw-alias-bg-layer-2, #2a2b31)",
|
|
904
|
+
transition: "background .15s, border-color .15s"
|
|
905
|
+
}
|
|
906
|
+
}, h("span", { style: {
|
|
907
|
+
position: "absolute",
|
|
908
|
+
top: 1.5,
|
|
909
|
+
left: 1.5,
|
|
910
|
+
width: THUMB,
|
|
911
|
+
height: THUMB,
|
|
912
|
+
borderRadius: "50%",
|
|
913
|
+
background: on ? "#fff" : "var(--dsw-alias-label-tertiary, #999)",
|
|
914
|
+
transform: on ? `translateX(${TRAVEL}px)` : "translateX(0)",
|
|
915
|
+
transition: "transform .15s, background .15s"
|
|
916
|
+
} }))), h("span", { style: {
|
|
917
|
+
fontSize: 13,
|
|
918
|
+
color: "var(--dsw-alias-label-primary, #e6e6e6)"
|
|
919
|
+
} }, text));
|
|
920
|
+
}
|
|
921
|
+
var TRACK_W, TRACK_H, THUMB, TRAVEL;
|
|
922
|
+
var init_toggle_switch = __esmMin((() => {
|
|
923
|
+
init_runtime();
|
|
924
|
+
TRACK_W = 30;
|
|
925
|
+
TRACK_H = 17;
|
|
926
|
+
THUMB = 12;
|
|
927
|
+
TRAVEL = 13;
|
|
928
|
+
}));
|
|
929
|
+
|
|
930
|
+
//#endregion
|
|
931
|
+
//#region src/client/cards.ts
|
|
932
|
+
/** 可折叠 section 卡:全宽头部按钮 + 旋转 chevron;open/onToggle 由 props 进。 */
|
|
933
|
+
function SectionCard({ title, open, onToggle, children, tt }) {
|
|
934
|
+
return h("div", { style: S.sectionCard }, h("button", {
|
|
935
|
+
type: "button",
|
|
936
|
+
style: S.sectionHead,
|
|
937
|
+
"aria-expanded": open,
|
|
938
|
+
"aria-label": `${tt(open ? "section.collapse" : "section.expand")}: ${title}`,
|
|
939
|
+
onClick: onToggle
|
|
940
|
+
}, h("span", { style: S.sectionHeadTitle }, title), h("svg", {
|
|
941
|
+
viewBox: "0 0 16 16",
|
|
942
|
+
width: 14,
|
|
943
|
+
height: 14,
|
|
944
|
+
fill: "none",
|
|
945
|
+
stroke: "currentColor",
|
|
946
|
+
strokeWidth: "1.5",
|
|
947
|
+
strokeLinecap: "round",
|
|
948
|
+
strokeLinejoin: "round",
|
|
949
|
+
"aria-hidden": "true",
|
|
950
|
+
style: open ? {
|
|
951
|
+
...S.chevron,
|
|
952
|
+
...S.chevronOpen
|
|
953
|
+
} : S.chevron
|
|
954
|
+
}, h("path", { d: "M3 6l5 5 5-5" }))), h("div", {
|
|
955
|
+
style: S.sectionBody,
|
|
956
|
+
hidden: !open
|
|
957
|
+
}, open ? children : null));
|
|
958
|
+
}
|
|
959
|
+
/** 官方魔粒余额头条:三格(可用/总额/冻结)+ 来源说明。null 渲染「—」。 */
|
|
960
|
+
function BalanceCard({ balance, tt }) {
|
|
961
|
+
if (balance === null || typeof balance !== "object") return null;
|
|
962
|
+
const row = balance;
|
|
963
|
+
if (row.error !== null && row.error !== void 0) return h("div", { style: {
|
|
964
|
+
...S.formNote,
|
|
965
|
+
color: "var(--dsw-alias-state-error-primary)",
|
|
966
|
+
margin: 0
|
|
967
|
+
} }, format(tt("balance.unavailable"), { error: row.error }));
|
|
968
|
+
if (row.available === null && row.total === null) return null;
|
|
969
|
+
const tile = (label, value) => h("div", { style: S.statCard }, h("div", { style: S.statValue }, typeof value === "number" && Number.isFinite(value) ? count(value) : "—"), h("div", { style: S.quotaLabel }, label));
|
|
970
|
+
return h("div", { style: S.card }, h("div", { style: S.statGrid }, tile(tt("balance.available"), row.available), tile(tt("balance.total"), row.total), tile(tt("balance.frozen"), row.frozen)), h("div", { style: S.trendLegend }, row.fetchedAt !== null && row.fetchedAt !== void 0 ? format(tt("balance.fetched"), { time: isoTime(row.fetchedAt) }) + " · " : "", tt("balance.note"), " ", h("a", {
|
|
971
|
+
style: S.formNote,
|
|
972
|
+
href: MODELSCOPE_USAGE_URL,
|
|
973
|
+
target: "_blank",
|
|
974
|
+
rel: "noreferrer"
|
|
975
|
+
}, tt("balance.usagePage"))));
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* 本地调用卡:纯统计口径——今日次数 + 按模型分布 + 一句口径说明。
|
|
979
|
+
* 刻意**不画**「推算剩余/参考上限」进度条:官方已改为魔粒计费,「2000 次」
|
|
980
|
+
* 是次数口径的社区快照,与魔粒余额并排展示是误导(README「三条事实」#2)。
|
|
981
|
+
* 本地计数回答的是官方余额回答不了的问题:哪个模型在烧、何时撞的 429。
|
|
982
|
+
*/
|
|
983
|
+
function LocalDailyCard({ snapshot, tt }) {
|
|
984
|
+
const quota = snapshot.quota;
|
|
985
|
+
const daily = quota?.daily ?? { usedLocal: 0 };
|
|
986
|
+
const perModel = Array.isArray(quota?.perModel) ? quota.perModel : [];
|
|
987
|
+
return h("div", { style: S.card }, h("div", { style: S.poolName }, format(tt("quota.headline"), {
|
|
988
|
+
calls: count(daily.usedLocal),
|
|
989
|
+
models: count(perModel.length)
|
|
990
|
+
})), h(ModelUsageTable, {
|
|
991
|
+
rows: perModel,
|
|
992
|
+
tt
|
|
993
|
+
}), h("div", { style: S.trendLegend }, tt("quota.note")));
|
|
994
|
+
}
|
|
995
|
+
/** 今日单模型排行(相对最大者画条)。 */
|
|
996
|
+
function ModelUsageTable({ rows, tt }) {
|
|
997
|
+
if (!Array.isArray(rows) || rows.length === 0) return h("div", { style: S.trendLegend }, tt("trend.none"));
|
|
998
|
+
const max = Math.max(0, ...rows.map((r) => Math.max(0, Number(r.calls) || 0)));
|
|
999
|
+
return h("div", null, rows.map((row) => {
|
|
1000
|
+
const pct = max > 0 ? row.calls / max * 100 : 0;
|
|
1001
|
+
return h("div", {
|
|
1002
|
+
key: row.modelId,
|
|
1003
|
+
style: S.trendRow
|
|
1004
|
+
}, h("div", { style: S.trendRowHead }, h("span", {
|
|
1005
|
+
style: S.trendModel,
|
|
1006
|
+
title: row.modelId
|
|
1007
|
+
}, row.modelId), h("span", { style: S.trendCredits }, count(row.calls) + (row.tokens !== null ? ` · ${count(row.tokens)}t` : ""))), pct >= 1 ? h("div", {
|
|
1008
|
+
style: S.trendBar,
|
|
1009
|
+
role: "progressbar",
|
|
1010
|
+
"aria-valuenow": Math.round(pct),
|
|
1011
|
+
"aria-valuemin": 0,
|
|
1012
|
+
"aria-valuemax": 100
|
|
1013
|
+
}, h("div", { style: {
|
|
1014
|
+
...S.barFill,
|
|
1015
|
+
width: `${pct}%`
|
|
1016
|
+
} })) : null);
|
|
1017
|
+
}), h("div", { style: S.trendLegend }, tt("trend.legend")));
|
|
1018
|
+
}
|
|
1019
|
+
/** 近 N 天本地调用趋势(天桶;缺桶 = 该日 0 次,真实零)。 */
|
|
1020
|
+
function TrendBars({ buckets, tt }) {
|
|
1021
|
+
if (!Array.isArray(buckets) || buckets.length === 0) return h("div", { style: S.trendLegend }, tt("trend.none"));
|
|
1022
|
+
const max = Math.max(0, ...buckets.map((b) => Math.max(0, b.calls)));
|
|
1023
|
+
return h("div", null, buckets.map((bucket) => h("div", {
|
|
1024
|
+
key: bucket.dateKey,
|
|
1025
|
+
style: S.trendRowHead
|
|
1026
|
+
}, h("span", { style: S.trendModel }, bucket.dateKey), h("div", {
|
|
1027
|
+
style: {
|
|
1028
|
+
...S.trendBar,
|
|
1029
|
+
flex: "1",
|
|
1030
|
+
marginLeft: 10
|
|
1031
|
+
},
|
|
1032
|
+
"aria-hidden": "true"
|
|
1033
|
+
}, max > 0 && bucket.calls > 0 ? h("div", { style: {
|
|
1034
|
+
...S.barFill,
|
|
1035
|
+
width: `${bucket.calls / max * 100}%`
|
|
1036
|
+
} }) : null), h("span", { style: S.quotaUsed }, String(bucket.calls)))), h("div", { style: S.trendLegend }, tt("trend.legend")));
|
|
1037
|
+
}
|
|
1038
|
+
/** 事件流(429/错误),Newest-first。 */
|
|
1039
|
+
function EventsList({ events, tt }) {
|
|
1040
|
+
if (!Array.isArray(events) || events.length === 0) return h("div", { style: S.trendLegend }, tt("events.none"));
|
|
1041
|
+
return h("div", null, events.map((event, index) => h("div", {
|
|
1042
|
+
key: `${event.at}-${index}`,
|
|
1043
|
+
style: S.trendRowHead
|
|
1044
|
+
}, h("span", { style: S.quotaUsed }, isoTime(event.at)), h("span", { style: S.modelTag }, tt(EVENT_KIND_KEY[event.kind])), h("span", {
|
|
1045
|
+
style: {
|
|
1046
|
+
...S.trendModel,
|
|
1047
|
+
flex: "1"
|
|
1048
|
+
},
|
|
1049
|
+
title: event.message
|
|
1050
|
+
}, event.message))));
|
|
1051
|
+
}
|
|
1052
|
+
/** 令牌表单:保存 / 忘掉 / 验令牌 + 状态行 + 外链。唯一持 state 的展示组件。 */
|
|
1053
|
+
function TokenForm({ token, busy, error, onSave, onForget, onVerify, verifyTitle, tt }) {
|
|
1054
|
+
const [value, setValue] = useState("");
|
|
1055
|
+
const source = token?.source ?? "none";
|
|
1056
|
+
const sourceKey = source === "credentials" ? "source.credentials" : source === "env" ? "source.env" : source === "memory" ? "source.memory" : "source.none";
|
|
1057
|
+
const validKey = token?.valid === true ? "validity.yes" : "validity.no";
|
|
1058
|
+
return h("div", null, h("div", { style: S.quotaUsed }, format(tt("token.status"), {
|
|
1059
|
+
present: token?.present ? tt("present.yes") : tt("present.no"),
|
|
1060
|
+
source: tt(sourceKey),
|
|
1061
|
+
valid: tt(validKey)
|
|
1062
|
+
})), token?.ephemeral ? h("div", { style: S.formNote }, tt("token.ephemeral")) : null, h("div", { style: S.rosterTools }, h("input", {
|
|
1063
|
+
style: S.input,
|
|
1064
|
+
type: "password",
|
|
1065
|
+
placeholder: tt("token.placeholder"),
|
|
1066
|
+
value,
|
|
1067
|
+
onChange: (event) => setValue(String(event.target.value ?? ""))
|
|
1068
|
+
}), h("button", {
|
|
1069
|
+
type: "button",
|
|
1070
|
+
style: S.primary,
|
|
1071
|
+
disabled: busy,
|
|
1072
|
+
onClick: () => {
|
|
1073
|
+
onSave(value);
|
|
1074
|
+
setValue("");
|
|
1075
|
+
}
|
|
1076
|
+
}, tt("token.save")), token?.present ? h("button", {
|
|
1077
|
+
type: "button",
|
|
1078
|
+
style: S.button,
|
|
1079
|
+
disabled: busy,
|
|
1080
|
+
onClick: onForget
|
|
1081
|
+
}, tt("token.forget")) : null, onVerify !== void 0 ? h("button", {
|
|
1082
|
+
type: "button",
|
|
1083
|
+
style: S.button,
|
|
1084
|
+
disabled: busy,
|
|
1085
|
+
onClick: onVerify,
|
|
1086
|
+
title: verifyTitle
|
|
1087
|
+
}, tt("probe.validity")) : null), error !== null ? h("div", {
|
|
1088
|
+
style: S.formError,
|
|
1089
|
+
role: "alert"
|
|
1090
|
+
}, error) : null, h("div", { style: S.formNote }, tt("token.hint")), h("a", {
|
|
1091
|
+
style: S.formNote,
|
|
1092
|
+
href: MODELSCOPE_TOKEN_URL,
|
|
1093
|
+
target: "_blank",
|
|
1094
|
+
rel: "noreferrer"
|
|
1095
|
+
}, tt("token.link")));
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* 接入为 DSH 模型的区块(开关 + 注册状态 + 允许清单)。
|
|
1099
|
+
*
|
|
1100
|
+
* 这是 M4 把抽象「试调」升级成真接入的落点:开关翻转即 POST provider 路由,
|
|
1101
|
+
* Host 在同一次请求里落盘并重注册适配器;允许清单勾完点「保存清单」即 POST
|
|
1102
|
+
* roster 路由。写盘(开关/清单)失败必须让面板看见,所以失败由调用方传进
|
|
1103
|
+
* `error` 一行渲染出来,不做乐观更新。
|
|
1104
|
+
*
|
|
1105
|
+
* 只有 ProviderCard 持 draft 状态(勾选清单),其余都是展示组件——与 TokenForm
|
|
1106
|
+
* 同一类 hook 边界,Node 渲染套件可以驱动无状态的部分。draft 在 Host 值真正
|
|
1107
|
+
* 变化时才跟过去(用 JSON 串做稳定信号,避免每次轮询都用新数组把在编辑的勾选
|
|
1108
|
+
* 冲掉);保存后由调用方 `load()` 刷新,快照回显的就是已落盘的值。
|
|
1109
|
+
*
|
|
1110
|
+
* 允许清单的语义(路由 §9 钉死):空清单 = 不过滤 = 全部提供,所以「全部」
|
|
1111
|
+
* 发 `[]`;「全部隐藏」发哨兵 `HIDE_ALL_MODELS`(「什么都不提供」)。哨兵
|
|
1112
|
+
* 不是任何真实模型 id,所以同一套 `draft.includes(id)` 的勾选判断天然不会把
|
|
1113
|
+
* 它画成勾中——不需要特判。
|
|
1114
|
+
*
|
|
1115
|
+
* 额度耗尽的模型保留在清单里(灰显、勾选框禁用),这是故意的:roster 是目录
|
|
1116
|
+
* 事实,picker 那边由 Host 自己丢掉它们,面板不替它删。
|
|
1117
|
+
*/
|
|
1118
|
+
function ProviderCard({ provider, busy, error, onToggle, onSaveList, onReset, tokenPresent, tt }) {
|
|
1119
|
+
const status = providerOf(provider);
|
|
1120
|
+
const enabled = status.enabled === true;
|
|
1121
|
+
const hostIds = status.enabledIds;
|
|
1122
|
+
const hostKey = useMemo(() => JSON.stringify(hostIds), [hostIds]);
|
|
1123
|
+
const [draft, setDraft] = useState(() => hostIds.slice());
|
|
1124
|
+
useEffect(() => {
|
|
1125
|
+
setDraft(hostIds);
|
|
1126
|
+
}, [hostKey]);
|
|
1127
|
+
const roster = status.roster;
|
|
1128
|
+
const rosterIds = roster.map((row) => row.id);
|
|
1129
|
+
const toggleOne = (id) => setDraft((current) => current.includes(id) ? current.filter((x) => x !== id) : [...current, id]);
|
|
1130
|
+
const ticked = roster.filter((row) => draft.includes(row.id)).length;
|
|
1131
|
+
const degraded = status.error === "unavailable";
|
|
1132
|
+
let statusNode;
|
|
1133
|
+
if (!degraded && typeof status.error === "string" && status.error !== "") statusNode = h("div", {
|
|
1134
|
+
style: S.formError,
|
|
1135
|
+
role: "alert"
|
|
1136
|
+
}, format(tt("provider.error"), { error: status.error }));
|
|
1137
|
+
else if (status.registered === true) statusNode = h("div", {
|
|
1138
|
+
style: {
|
|
1139
|
+
fontSize: 12,
|
|
1140
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
1141
|
+
},
|
|
1142
|
+
role: "status"
|
|
1143
|
+
}, tt("provider.registered"));
|
|
1144
|
+
else if (status.llmAvailable !== true) statusNode = h("div", { style: {
|
|
1145
|
+
...S.formNote,
|
|
1146
|
+
color: "var(--dsw-alias-state-warn-primary)"
|
|
1147
|
+
} }, tt("provider.llmMissing"));
|
|
1148
|
+
else statusNode = h("div", {
|
|
1149
|
+
style: {
|
|
1150
|
+
...S.muted,
|
|
1151
|
+
fontSize: 12
|
|
1152
|
+
},
|
|
1153
|
+
role: "status"
|
|
1154
|
+
}, tt("provider.notRegistered"));
|
|
1155
|
+
const tokenHint = enabled && tokenPresent === false ? h("div", { style: {
|
|
1156
|
+
...S.formNote,
|
|
1157
|
+
color: "var(--dsw-alias-state-warn-primary)"
|
|
1158
|
+
} }, tt("provider.notConfigured")) : null;
|
|
1159
|
+
const sourceKey = status.source === "panel" ? "provider.source.panel" : "provider.source.config";
|
|
1160
|
+
const switchRow = h("div", { style: {
|
|
1161
|
+
display: "flex",
|
|
1162
|
+
gap: 10,
|
|
1163
|
+
alignItems: "center",
|
|
1164
|
+
flexWrap: "wrap",
|
|
1165
|
+
margin: "0 0 8px"
|
|
1166
|
+
} }, h(ToggleSwitch, {
|
|
1167
|
+
checked: enabled,
|
|
1168
|
+
onChange: () => onToggle(!enabled),
|
|
1169
|
+
busy,
|
|
1170
|
+
label: tt(enabled ? "provider.on" : "provider.off"),
|
|
1171
|
+
title: tt("provider.enable")
|
|
1172
|
+
}), h("span", { style: {
|
|
1173
|
+
fontSize: 11,
|
|
1174
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
1175
|
+
} }, tt(sourceKey)));
|
|
1176
|
+
const rosterBlock = roster.length === 0 ? h("div", { style: {
|
|
1177
|
+
...S.muted,
|
|
1178
|
+
fontSize: 12,
|
|
1179
|
+
marginTop: 6
|
|
1180
|
+
} }, tt("provider.rosterHint")) : h("div", { style: S.modelPanel }, h("div", { style: S.rosterTools }, h("span", {
|
|
1181
|
+
style: S.rosterCount,
|
|
1182
|
+
title: format(tt("provider.enabledCount"), {
|
|
1183
|
+
count: ticked,
|
|
1184
|
+
total: roster.length
|
|
1185
|
+
})
|
|
1186
|
+
}, format(tt("provider.enabledCount"), {
|
|
1187
|
+
count: ticked,
|
|
1188
|
+
total: roster.length
|
|
1189
|
+
})), h("button", {
|
|
1190
|
+
type: "button",
|
|
1191
|
+
style: S.rosterBulk,
|
|
1192
|
+
disabled: busy,
|
|
1193
|
+
onClick: () => {
|
|
1194
|
+
setDraft(rosterIds.slice());
|
|
1195
|
+
onSaveList(rosterIds.slice());
|
|
1196
|
+
}
|
|
1197
|
+
}, tt("provider.allowAll")), h("button", {
|
|
1198
|
+
type: "button",
|
|
1199
|
+
style: S.rosterBulk,
|
|
1200
|
+
disabled: busy,
|
|
1201
|
+
onClick: () => {
|
|
1202
|
+
setDraft([HIDE_ALL_MODELS]);
|
|
1203
|
+
onSaveList([HIDE_ALL_MODELS]);
|
|
1204
|
+
}
|
|
1205
|
+
}, tt("provider.hideAll")), h("button", {
|
|
1206
|
+
type: "button",
|
|
1207
|
+
style: S.primary,
|
|
1208
|
+
disabled: busy,
|
|
1209
|
+
onClick: () => onSaveList(draft.slice())
|
|
1210
|
+
}, tt("provider.saveList"))), h("ul", {
|
|
1211
|
+
style: S.modelList,
|
|
1212
|
+
role: "list"
|
|
1213
|
+
}, roster.map((row) => {
|
|
1214
|
+
const id = row.id;
|
|
1215
|
+
const unusable = row.available === false || row.quotaExhausted === true;
|
|
1216
|
+
return h("li", {
|
|
1217
|
+
key: id,
|
|
1218
|
+
style: unusable ? {
|
|
1219
|
+
...S.modelRow,
|
|
1220
|
+
...S.modelRowOff
|
|
1221
|
+
} : S.modelRow
|
|
1222
|
+
}, h("div", { style: S.modelRowHead }, h("label", { style: {
|
|
1223
|
+
display: "flex",
|
|
1224
|
+
alignItems: "center",
|
|
1225
|
+
gap: 10,
|
|
1226
|
+
flex: "1 1 auto",
|
|
1227
|
+
minWidth: 0,
|
|
1228
|
+
cursor: busy || unusable ? "default" : "pointer"
|
|
1229
|
+
} }, h("input", {
|
|
1230
|
+
type: "checkbox",
|
|
1231
|
+
checked: draft.includes(id),
|
|
1232
|
+
disabled: busy || unusable,
|
|
1233
|
+
onChange: () => toggleOne(id),
|
|
1234
|
+
style: S.modelCheck,
|
|
1235
|
+
"aria-label": id
|
|
1236
|
+
}), h("span", {
|
|
1237
|
+
style: S.modelName,
|
|
1238
|
+
title: id
|
|
1239
|
+
}, row.name)), row.vision === true ? h("span", { style: S.modelBadge }, tt("provider.vision")) : null, FEATURED_OWNERS.includes(catalogOwner(row.id)) ? h("span", {
|
|
1240
|
+
style: S.modelBadgeFeatured,
|
|
1241
|
+
title: tt("models.featuredTitle")
|
|
1242
|
+
}, tt("provider.featured")) : null, row.quotaExhausted === true ? h("span", { style: {
|
|
1243
|
+
...S.modelBadge,
|
|
1244
|
+
color: "var(--dsw-alias-state-error-primary)"
|
|
1245
|
+
} }, tt("provider.quotaExhausted")) : null, h("a", {
|
|
1246
|
+
style: {
|
|
1247
|
+
...S.formNote,
|
|
1248
|
+
margin: 0,
|
|
1249
|
+
marginLeft: "auto"
|
|
1250
|
+
},
|
|
1251
|
+
href: modelscopeModelUrl(row.id),
|
|
1252
|
+
target: "_blank",
|
|
1253
|
+
rel: "noreferrer",
|
|
1254
|
+
title: tt("models.viewOnSiteTitle")
|
|
1255
|
+
}, tt("models.viewOnSite"))));
|
|
1256
|
+
})), h("div", { style: S.rosterFoot }, h("button", {
|
|
1257
|
+
type: "button",
|
|
1258
|
+
style: S.button,
|
|
1259
|
+
disabled: busy,
|
|
1260
|
+
onClick: onReset
|
|
1261
|
+
}, tt("provider.reset"))));
|
|
1262
|
+
return h("div", null, switchRow, statusNode, tokenHint, h("div", { style: {
|
|
1263
|
+
...S.sectionTitle,
|
|
1264
|
+
margin: "18px 0 8px"
|
|
1265
|
+
} }, tt("provider.models")), rosterBlock, error !== null ? h("div", {
|
|
1266
|
+
style: S.formError,
|
|
1267
|
+
role: "alert"
|
|
1268
|
+
}, error) : null);
|
|
1269
|
+
}
|
|
1270
|
+
var EVENT_KIND_KEY;
|
|
1271
|
+
var init_cards = __esmMin((() => {
|
|
1272
|
+
init_format();
|
|
1273
|
+
init_runtime();
|
|
1274
|
+
init_styles();
|
|
1275
|
+
init_const();
|
|
1276
|
+
init_snapshot();
|
|
1277
|
+
init_toggle_switch();
|
|
1278
|
+
EVENT_KIND_KEY = {
|
|
1279
|
+
quota: "events.quota",
|
|
1280
|
+
rate_limit: "events.rate_limit",
|
|
1281
|
+
error: "events.error"
|
|
1282
|
+
};
|
|
1283
|
+
}));
|
|
1284
|
+
|
|
1285
|
+
//#endregion
|
|
1286
|
+
//#region src/client/http.ts
|
|
1287
|
+
/** POST 并解析 body;非 JSON 响应返回 null。 */
|
|
1288
|
+
async function postJson(path, payload) {
|
|
1289
|
+
return await (await fetch(path, {
|
|
1290
|
+
method: "POST",
|
|
1291
|
+
headers: {
|
|
1292
|
+
"content-type": "application/json",
|
|
1293
|
+
accept: "application/json"
|
|
1294
|
+
},
|
|
1295
|
+
cache: "no-store",
|
|
1296
|
+
body: JSON.stringify(payload)
|
|
1297
|
+
})).json().catch(() => null);
|
|
1298
|
+
}
|
|
1299
|
+
/** GET 同源 JSON;非 2xx 或非 JSON 返回 null(调用方决定怎么降级)。 */
|
|
1300
|
+
async function getJson(path) {
|
|
1301
|
+
const response = await fetch(path, {
|
|
1302
|
+
headers: { accept: "application/json" },
|
|
1303
|
+
cache: "no-store"
|
|
1304
|
+
});
|
|
1305
|
+
if (!response.ok) return null;
|
|
1306
|
+
return await response.json().catch(() => null);
|
|
1307
|
+
}
|
|
1308
|
+
var init_http = __esmMin((() => {}));
|
|
1309
|
+
|
|
1310
|
+
//#endregion
|
|
1311
|
+
//#region src/client/use-polling-interval.ts
|
|
1312
|
+
/**
|
|
1313
|
+
* Run `run()` on an interval that stops while the page is hidden, and slows
|
|
1314
|
+
* down while `failed` is true.
|
|
1315
|
+
*
|
|
1316
|
+
* @param run - the poll body; must be stable (wrap it in `useCallback`), since
|
|
1317
|
+
* it is an effect dependency and a fresh identity would restart the loop on
|
|
1318
|
+
* every render.
|
|
1319
|
+
* @param intervalMs - the healthy cadence, in milliseconds.
|
|
1320
|
+
* @param options - loop control.
|
|
1321
|
+
* @param options.enabled - when false the loop does not run at all (the caller
|
|
1322
|
+
* has decided polling is not wanted); defaults to true.
|
|
1323
|
+
* @param options.failed - when true the loop backs off to
|
|
1324
|
+
* {@link ERROR_BACKOFF_MS}, never faster than `intervalMs`.
|
|
1325
|
+
*/
|
|
1326
|
+
function usePollingInterval(run, intervalMs, options = {}) {
|
|
1327
|
+
const { enabled = true, failed = false } = options;
|
|
1328
|
+
const healthy = Math.max(1, Math.floor(intervalMs));
|
|
1329
|
+
const effective = failed ? Math.max(healthy, ERROR_BACKOFF_MS) : healthy;
|
|
1330
|
+
const runRef = useRef(run);
|
|
1331
|
+
runRef.current = run;
|
|
1332
|
+
useEffect(() => {
|
|
1333
|
+
if (!enabled) return;
|
|
1334
|
+
let alive = true;
|
|
1335
|
+
let timer = null;
|
|
1336
|
+
const fire = () => {
|
|
1337
|
+
if (alive) runRef.current();
|
|
1338
|
+
};
|
|
1339
|
+
const start = () => {
|
|
1340
|
+
if (timer === null) timer = setInterval(fire, effective);
|
|
1341
|
+
};
|
|
1342
|
+
const stop = () => {
|
|
1343
|
+
if (timer !== null) {
|
|
1344
|
+
clearInterval(timer);
|
|
1345
|
+
timer = null;
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
const hidden = () => typeof document !== "undefined" && document.visibilityState === "hidden";
|
|
1349
|
+
if (!hidden()) {
|
|
1350
|
+
fire();
|
|
1351
|
+
start();
|
|
1352
|
+
}
|
|
1353
|
+
const onVisibility = () => {
|
|
1354
|
+
if (!alive) return;
|
|
1355
|
+
if (hidden()) stop();
|
|
1356
|
+
else {
|
|
1357
|
+
fire();
|
|
1358
|
+
start();
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
if (typeof document !== "undefined" && "addEventListener" in document) document.addEventListener("visibilitychange", onVisibility);
|
|
1362
|
+
return () => {
|
|
1363
|
+
alive = false;
|
|
1364
|
+
stop();
|
|
1365
|
+
if (typeof document !== "undefined" && "addEventListener" in document) document.removeEventListener("visibilitychange", onVisibility);
|
|
1366
|
+
};
|
|
1367
|
+
}, [effective, enabled]);
|
|
1368
|
+
}
|
|
1369
|
+
var ERROR_BACKOFF_MS;
|
|
1370
|
+
var init_use_polling_interval = __esmMin((() => {
|
|
1371
|
+
init_runtime();
|
|
1372
|
+
ERROR_BACKOFF_MS = 6e4;
|
|
1373
|
+
}));
|
|
1374
|
+
|
|
1375
|
+
//#endregion
|
|
1376
|
+
//#region src/client/use-snapshot-polling.ts
|
|
1377
|
+
function useSnapshotPolling(defaultCadenceMs = 3e4) {
|
|
1378
|
+
const [data, setData] = useState(null);
|
|
1379
|
+
const [error, setError] = useState(null);
|
|
1380
|
+
const [loadedOnce, setLoadedOnce] = useState(false);
|
|
1381
|
+
const [updatedAt, setUpdatedAt] = useState(0);
|
|
1382
|
+
const [cadenceMs, setCadenceMs] = useState(defaultCadenceMs);
|
|
1383
|
+
/** Host 少给的顶层键(契约漂移);透给面板当 shapeWarning,不静默。 */
|
|
1384
|
+
const [missingKeys, setMissingKeys] = useState([]);
|
|
1385
|
+
const generation = useRef(0);
|
|
1386
|
+
const inFlight = useRef(null);
|
|
1387
|
+
const cadenceRef = useRef(cadenceMs);
|
|
1388
|
+
cadenceRef.current = cadenceMs;
|
|
1389
|
+
const load = useCallback(async () => {
|
|
1390
|
+
generation.current += 1;
|
|
1391
|
+
const mine = generation.current;
|
|
1392
|
+
const isCurrent = () => generation.current === mine;
|
|
1393
|
+
inFlight.current?.abort?.();
|
|
1394
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
1395
|
+
inFlight.current = controller;
|
|
1396
|
+
try {
|
|
1397
|
+
const response = await fetch(SNAPSHOT_PATH, {
|
|
1398
|
+
headers: { accept: "application/json" },
|
|
1399
|
+
cache: "no-store",
|
|
1400
|
+
...controller ? { signal: controller.signal } : {}
|
|
1401
|
+
});
|
|
1402
|
+
if (!isCurrent()) return;
|
|
1403
|
+
if (!response.ok) {
|
|
1404
|
+
setError(errorOfStatus(response.status));
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const body = await response.json().catch(() => null);
|
|
1408
|
+
if (!isCurrent()) return;
|
|
1409
|
+
const read = interpretSnapshot(body);
|
|
1410
|
+
if (read.data === null) {
|
|
1411
|
+
setData(null);
|
|
1412
|
+
setMissingKeys([]);
|
|
1413
|
+
setError(read.error);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
setData(read.data);
|
|
1417
|
+
setMissingKeys(read.missingKeys ?? []);
|
|
1418
|
+
setError(null);
|
|
1419
|
+
setUpdatedAt(Date.now());
|
|
1420
|
+
const stated = read.data?.pollSeconds;
|
|
1421
|
+
if (typeof stated === "number" && Number.isFinite(stated)) setCadenceMs(statedCadenceMs(stated, cadenceRef.current));
|
|
1422
|
+
} catch (reason) {
|
|
1423
|
+
if (!isCurrent()) return;
|
|
1424
|
+
setError(errorText(reason));
|
|
1425
|
+
} finally {
|
|
1426
|
+
if (isCurrent()) setLoadedOnce(true);
|
|
1427
|
+
if (inFlight.current === controller) inFlight.current = null;
|
|
1428
|
+
}
|
|
1429
|
+
}, []);
|
|
1430
|
+
usePollingInterval(load, cadenceMs, { failed: error !== null });
|
|
1431
|
+
useEffect(() => () => {
|
|
1432
|
+
generation.current += 1;
|
|
1433
|
+
inFlight.current?.abort?.();
|
|
1434
|
+
}, []);
|
|
1435
|
+
return {
|
|
1436
|
+
data,
|
|
1437
|
+
error,
|
|
1438
|
+
loadedOnce,
|
|
1439
|
+
updatedAt,
|
|
1440
|
+
load,
|
|
1441
|
+
missingKeys
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
var init_use_snapshot_polling = __esmMin((() => {
|
|
1445
|
+
init_runtime();
|
|
1446
|
+
init_snapshot();
|
|
1447
|
+
init_format();
|
|
1448
|
+
init_use_polling_interval();
|
|
1449
|
+
init_const();
|
|
1450
|
+
}));
|
|
1451
|
+
|
|
1452
|
+
//#endregion
|
|
1453
|
+
//#region src/client/panel-page.ts
|
|
1454
|
+
function PanelPage({ tt, localeSubscribe }) {
|
|
1455
|
+
const { data, error, loadedOnce, updatedAt, load, missingKeys } = useSnapshotPolling();
|
|
1456
|
+
const [, setLocaleRevision] = useState(0);
|
|
1457
|
+
const [openSections, setOpenSections] = useState({
|
|
1458
|
+
balance: true,
|
|
1459
|
+
local: true,
|
|
1460
|
+
trend: true,
|
|
1461
|
+
events: false,
|
|
1462
|
+
provider: true,
|
|
1463
|
+
token: true
|
|
1464
|
+
});
|
|
1465
|
+
const [activeTab, setActiveTab] = useState("quota");
|
|
1466
|
+
useEffect(() => {
|
|
1467
|
+
if (typeof localeSubscribe !== "function") return void 0;
|
|
1468
|
+
return localeSubscribe(() => setLocaleRevision((revision) => revision + 1));
|
|
1469
|
+
}, [localeSubscribe]);
|
|
1470
|
+
const toggleSection = useCallback((key) => {
|
|
1471
|
+
setOpenSections((current) => ({
|
|
1472
|
+
...current,
|
|
1473
|
+
[key]: !current[key]
|
|
1474
|
+
}));
|
|
1475
|
+
}, []);
|
|
1476
|
+
const { failure, needsSetup, guidance, shapeWarnings } = viewOf(data, error, tt, missingKeys);
|
|
1477
|
+
const showSetup = needsSetup && loadedOnce;
|
|
1478
|
+
const rawProvider = data?.provider;
|
|
1479
|
+
const hasProviderBlock = rawProvider !== void 0 && rawProvider !== null;
|
|
1480
|
+
const [fetchedProvider, setFetchedProvider] = useState(null);
|
|
1481
|
+
const provider = hasProviderBlock ? providerOf(rawProvider) : fetchedProvider ?? DEGRADED_PROVIDER;
|
|
1482
|
+
useEffect(() => {
|
|
1483
|
+
if (hasProviderBlock) return void 0;
|
|
1484
|
+
let cancelled = false;
|
|
1485
|
+
getJson(PROVIDER_PATH).then((body) => {
|
|
1486
|
+
if (!cancelled && body !== null && body.ok === true) setFetchedProvider(providerOf(body));
|
|
1487
|
+
}).catch(() => {});
|
|
1488
|
+
return () => {
|
|
1489
|
+
cancelled = true;
|
|
1490
|
+
};
|
|
1491
|
+
}, [hasProviderBlock]);
|
|
1492
|
+
const [providerBusy, setProviderBusy] = useState(false);
|
|
1493
|
+
const [providerError, setProviderError] = useState(null);
|
|
1494
|
+
const runProviderWrite = useCallback(async (path, payload) => {
|
|
1495
|
+
setProviderBusy(true);
|
|
1496
|
+
setProviderError(null);
|
|
1497
|
+
try {
|
|
1498
|
+
const body = await postJson(path, payload);
|
|
1499
|
+
if (body === null || body.ok !== true) throw new Error(typeof body?.error === "string" ? body.error : "HTTP error");
|
|
1500
|
+
load();
|
|
1501
|
+
} catch (reason) {
|
|
1502
|
+
setProviderError(format(tt("provider.error"), { error: errorText(reason) }));
|
|
1503
|
+
} finally {
|
|
1504
|
+
setProviderBusy(false);
|
|
1505
|
+
}
|
|
1506
|
+
}, [load, tt]);
|
|
1507
|
+
const toggleProvider = useCallback((enabled) => void runProviderWrite(PROVIDER_PATH, { enabled }), [runProviderWrite]);
|
|
1508
|
+
const saveRoster = useCallback((enabledIds) => void runProviderWrite(PROVIDER_ROSTER_PATH, { enabledIds }), [runProviderWrite]);
|
|
1509
|
+
const resetProvider = useCallback(() => void runProviderWrite(PROVIDER_RESET_PATH, {}), [runProviderWrite]);
|
|
1510
|
+
const [tokenBusy, setTokenBusy] = useState(false);
|
|
1511
|
+
const [tokenError, setTokenError] = useState(null);
|
|
1512
|
+
const saveToken = useCallback(async (value) => {
|
|
1513
|
+
setTokenBusy(true);
|
|
1514
|
+
setTokenError(null);
|
|
1515
|
+
try {
|
|
1516
|
+
const body = await postJson(TOKEN_PATH, { token: value });
|
|
1517
|
+
if (body === null || body.ok !== true) throw new Error(typeof body?.error === "string" ? body.error : "HTTP error");
|
|
1518
|
+
load();
|
|
1519
|
+
} catch (reason) {
|
|
1520
|
+
setTokenError(errorText(reason));
|
|
1521
|
+
} finally {
|
|
1522
|
+
setTokenBusy(false);
|
|
1523
|
+
}
|
|
1524
|
+
}, [load]);
|
|
1525
|
+
const forgetToken = useCallback(async () => {
|
|
1526
|
+
setTokenBusy(true);
|
|
1527
|
+
setTokenError(null);
|
|
1528
|
+
try {
|
|
1529
|
+
await postJson(TOKEN_FORGET_PATH, {});
|
|
1530
|
+
load();
|
|
1531
|
+
} catch (reason) {
|
|
1532
|
+
setTokenError(errorText(reason));
|
|
1533
|
+
} finally {
|
|
1534
|
+
setTokenBusy(false);
|
|
1535
|
+
}
|
|
1536
|
+
}, [load]);
|
|
1537
|
+
const tokenState = data?.token ?? null;
|
|
1538
|
+
const sampleModel = data?.models?.sample?.[0] ?? null;
|
|
1539
|
+
const [verifyBusy, setVerifyBusy] = useState(false);
|
|
1540
|
+
const [verifyNote, setVerifyNote] = useState(null);
|
|
1541
|
+
const [verifyError, setVerifyError] = useState(null);
|
|
1542
|
+
const runValidity = useCallback(async (modelId) => {
|
|
1543
|
+
setVerifyBusy(true);
|
|
1544
|
+
setVerifyNote(null);
|
|
1545
|
+
setVerifyError(null);
|
|
1546
|
+
try {
|
|
1547
|
+
const body = await postJson(PROBE_PATH, {
|
|
1548
|
+
modelId,
|
|
1549
|
+
kind: "validity"
|
|
1550
|
+
});
|
|
1551
|
+
if (body !== null && body.ok === true) setVerifyNote(format(tt("probe.validOk"), { status: Number(body.status ?? 0) }));
|
|
1552
|
+
else setVerifyError(typeof body?.error === "string" ? body.error : "HTTP error");
|
|
1553
|
+
} catch (reason) {
|
|
1554
|
+
setVerifyError(errorText(reason));
|
|
1555
|
+
} finally {
|
|
1556
|
+
setVerifyBusy(false);
|
|
1557
|
+
}
|
|
1558
|
+
}, [tt]);
|
|
1559
|
+
const quotaBody = () => {
|
|
1560
|
+
if (data === null) return h("div", { style: S.empty }, failure === null ? tt("panel.loading") : h("div", { role: "alert" }, guidance ?? format(tt("panel.error"), { error: failure.message })));
|
|
1561
|
+
if (showSetup && failure !== null) return h("div", {
|
|
1562
|
+
style: S.empty,
|
|
1563
|
+
role: "alert"
|
|
1564
|
+
}, guidance ?? format(tt("panel.error"), { error: failure.message }));
|
|
1565
|
+
const snap = data;
|
|
1566
|
+
return h("div", null, snap.token?.present === false ? h("div", {
|
|
1567
|
+
style: S.formNote,
|
|
1568
|
+
role: "status"
|
|
1569
|
+
}, tt("panel.noToken")) : null, h(SectionCard, {
|
|
1570
|
+
title: tt("section.balance"),
|
|
1571
|
+
open: openSections.balance,
|
|
1572
|
+
onToggle: () => toggleSection("balance"),
|
|
1573
|
+
tt
|
|
1574
|
+
}, h(BalanceCard, {
|
|
1575
|
+
balance: snap.balance,
|
|
1576
|
+
tt
|
|
1577
|
+
})), h(SectionCard, {
|
|
1578
|
+
title: tt("section.local"),
|
|
1579
|
+
open: openSections.local,
|
|
1580
|
+
onToggle: () => toggleSection("local"),
|
|
1581
|
+
tt
|
|
1582
|
+
}, h(LocalDailyCard, {
|
|
1583
|
+
snapshot: snap,
|
|
1584
|
+
tt
|
|
1585
|
+
})), h(SectionCard, {
|
|
1586
|
+
title: format(tt("section.trend"), { days: snap.trend?.days ?? 0 }),
|
|
1587
|
+
open: openSections.trend,
|
|
1588
|
+
onToggle: () => toggleSection("trend"),
|
|
1589
|
+
tt
|
|
1590
|
+
}, h(TrendBars, {
|
|
1591
|
+
buckets: snap.trend?.buckets ?? [],
|
|
1592
|
+
tt
|
|
1593
|
+
})), h(SectionCard, {
|
|
1594
|
+
title: tt("section.events"),
|
|
1595
|
+
open: openSections.events,
|
|
1596
|
+
onToggle: () => toggleSection("events"),
|
|
1597
|
+
tt
|
|
1598
|
+
}, h(EventsList, {
|
|
1599
|
+
events: snap.events ?? [],
|
|
1600
|
+
tt
|
|
1601
|
+
})));
|
|
1602
|
+
};
|
|
1603
|
+
const modelsBody = () => h("div", null, h(SectionCard, {
|
|
1604
|
+
title: tt("section.provider"),
|
|
1605
|
+
open: openSections.provider,
|
|
1606
|
+
onToggle: () => toggleSection("provider"),
|
|
1607
|
+
tt
|
|
1608
|
+
}, h(ProviderCard, {
|
|
1609
|
+
provider,
|
|
1610
|
+
busy: providerBusy,
|
|
1611
|
+
error: providerError,
|
|
1612
|
+
onToggle: toggleProvider,
|
|
1613
|
+
onSaveList: saveRoster,
|
|
1614
|
+
onReset: resetProvider,
|
|
1615
|
+
tokenPresent: data?.token?.present === true,
|
|
1616
|
+
tt
|
|
1617
|
+
})));
|
|
1618
|
+
const accessBody = () => h("div", null, h(SectionCard, {
|
|
1619
|
+
title: tt("section.token"),
|
|
1620
|
+
open: openSections.token,
|
|
1621
|
+
onToggle: () => toggleSection("token"),
|
|
1622
|
+
tt
|
|
1623
|
+
}, h(TokenForm, {
|
|
1624
|
+
token: tokenState,
|
|
1625
|
+
busy: tokenBusy || verifyBusy,
|
|
1626
|
+
error: tokenError,
|
|
1627
|
+
onSave: (value) => void saveToken(value),
|
|
1628
|
+
onForget: () => void forgetToken(),
|
|
1629
|
+
onVerify: sampleModel === null ? void 0 : () => void runValidity(sampleModel),
|
|
1630
|
+
verifyTitle: sampleModel ?? void 0,
|
|
1631
|
+
tt
|
|
1632
|
+
}), verifyNote !== null ? h("div", {
|
|
1633
|
+
style: S.formNote,
|
|
1634
|
+
role: "status"
|
|
1635
|
+
}, verifyNote) : null, verifyError !== null ? h("div", {
|
|
1636
|
+
style: S.formError,
|
|
1637
|
+
role: "alert"
|
|
1638
|
+
}, format(tt("probe.fail"), { error: verifyError })) : null));
|
|
1639
|
+
return h("div", {
|
|
1640
|
+
style: S.page,
|
|
1641
|
+
"data-dsh-plugin": PANEL_ID
|
|
1642
|
+
}, h("div", { style: S.headerBar }, h("div", { style: S.header }, h("h1", { style: S.title }, tt("panel.title")), h("span", { style: S.spacer }), h("span", { style: S.cluster }, data !== null ? h("span", { style: S.updated }, format(tt("panel.updated"), { time: isoTime(new Date(updatedAt).toISOString()) })) : null, failure !== null && data !== null ? h("span", {
|
|
1643
|
+
style: S.error,
|
|
1644
|
+
role: "status",
|
|
1645
|
+
title: failure.message
|
|
1646
|
+
}, format(tt("panel.error"), { error: failure.message })) : null, h("button", {
|
|
1647
|
+
type: "button",
|
|
1648
|
+
style: S.button,
|
|
1649
|
+
onClick: () => void load()
|
|
1650
|
+
}, tt("panel.refresh"))))), h("div", { style: S.scroll }, h("div", { style: S.content }, h("div", null, h("div", {
|
|
1651
|
+
style: S.tabBar,
|
|
1652
|
+
role: "tablist"
|
|
1653
|
+
}, h("button", {
|
|
1654
|
+
type: "button",
|
|
1655
|
+
role: "tab",
|
|
1656
|
+
"aria-selected": activeTab === "quota",
|
|
1657
|
+
style: {
|
|
1658
|
+
...S.tab,
|
|
1659
|
+
...activeTab === "quota" ? S.tabActive : {}
|
|
1660
|
+
},
|
|
1661
|
+
onClick: () => setActiveTab("quota")
|
|
1662
|
+
}, tt("tab.quota")), h("button", {
|
|
1663
|
+
type: "button",
|
|
1664
|
+
role: "tab",
|
|
1665
|
+
"aria-selected": activeTab === "models",
|
|
1666
|
+
style: {
|
|
1667
|
+
...S.tab,
|
|
1668
|
+
...activeTab === "models" ? S.tabActive : {}
|
|
1669
|
+
},
|
|
1670
|
+
onClick: () => setActiveTab("models")
|
|
1671
|
+
}, tt("tab.models")), h("button", {
|
|
1672
|
+
type: "button",
|
|
1673
|
+
role: "tab",
|
|
1674
|
+
"aria-selected": activeTab === "access",
|
|
1675
|
+
style: {
|
|
1676
|
+
...S.tab,
|
|
1677
|
+
...activeTab === "access" ? S.tabActive : {}
|
|
1678
|
+
},
|
|
1679
|
+
onClick: () => setActiveTab("access")
|
|
1680
|
+
}, tt("tab.access"))), data !== null && shapeWarnings.length > 0 ? h("div", {
|
|
1681
|
+
style: S.formError,
|
|
1682
|
+
role: "status"
|
|
1683
|
+
}, format(tt("panel.shapeDrift"), { detail: shapeWarnings.join("; ") })) : null, activeTab === "quota" ? quotaBody() : activeTab === "models" ? modelsBody() : accessBody()))));
|
|
1684
|
+
}
|
|
1685
|
+
var init_panel_page = __esmMin((() => {
|
|
1686
|
+
init_cards();
|
|
1687
|
+
init_const();
|
|
1688
|
+
init_format();
|
|
1689
|
+
init_http();
|
|
1690
|
+
init_snapshot();
|
|
1691
|
+
init_use_snapshot_polling();
|
|
1692
|
+
init_runtime();
|
|
1693
|
+
init_styles();
|
|
1694
|
+
}));
|
|
1695
|
+
|
|
1696
|
+
//#endregion
|
|
1697
|
+
//#region src/client/apply.ts
|
|
1698
|
+
/**
|
|
1699
|
+
* 注册字典与配置卡。卡片由 Host 的 renderSlot("plugins.bundle.config", …)
|
|
1700
|
+
* 渲染;本卡无关闭按钮——导航归 Plugins 页所有。
|
|
1701
|
+
*/
|
|
1702
|
+
function apply(ctx) {
|
|
1703
|
+
ctx.effect(() => {
|
|
1704
|
+
try {
|
|
1705
|
+
return ctx.locale.register(NS, {
|
|
1706
|
+
zh,
|
|
1707
|
+
en
|
|
1708
|
+
});
|
|
1709
|
+
} catch {
|
|
1710
|
+
return () => {};
|
|
1711
|
+
}
|
|
1712
|
+
}, `${NS}: dictionaries`);
|
|
1713
|
+
let translate = (key) => key;
|
|
1714
|
+
try {
|
|
1715
|
+
translate = ctx.locale.bind(NS);
|
|
1716
|
+
} catch {}
|
|
1717
|
+
const tt = (key) => {
|
|
1718
|
+
try {
|
|
1719
|
+
return translate(key);
|
|
1720
|
+
} catch {
|
|
1721
|
+
return key;
|
|
1722
|
+
}
|
|
1723
|
+
};
|
|
1724
|
+
const disposers = [];
|
|
1725
|
+
try {
|
|
1726
|
+
disposers.push(ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
|
|
1727
|
+
name: "plugins.bundle.config",
|
|
1728
|
+
key: NS,
|
|
1729
|
+
locale: NS,
|
|
1730
|
+
inject: () => ({
|
|
1731
|
+
tt,
|
|
1732
|
+
localeSubscribe: ctx.locale.subscribe.bind(ctx.locale)
|
|
1733
|
+
})
|
|
1734
|
+
}, PanelPage)));
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
console.warn(`[${NS}] config card registration failed:`, error);
|
|
1737
|
+
}
|
|
1738
|
+
ctx.effect(() => () => {
|
|
1739
|
+
for (const dispose of disposers.splice(0)) try {
|
|
1740
|
+
dispose();
|
|
1741
|
+
} catch {}
|
|
1742
|
+
}, `${NS}: ui mounts`);
|
|
1743
|
+
}
|
|
1744
|
+
var inject;
|
|
1745
|
+
var init_apply = __esmMin((() => {
|
|
1746
|
+
init_const();
|
|
1747
|
+
init_i18n();
|
|
1748
|
+
init_panel_page();
|
|
1749
|
+
inject = ["slots", "locale"];
|
|
1750
|
+
}));
|
|
1751
|
+
|
|
1752
|
+
//#endregion
|
|
1753
|
+
//#region src/client/index.ts
|
|
1754
|
+
var require_client = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
1755
|
+
init_apply();
|
|
1756
|
+
init_const();
|
|
1757
|
+
init_snapshot();
|
|
1758
|
+
init_format();
|
|
1759
|
+
init_runtime();
|
|
1760
|
+
init_i18n();
|
|
1761
|
+
init_styles();
|
|
1762
|
+
init_cards();
|
|
1763
|
+
init_panel_page();
|
|
1764
|
+
init_use_polling_interval();
|
|
1765
|
+
init_use_snapshot_polling();
|
|
1766
|
+
init_http();
|
|
1767
|
+
function clientFactory(loaderRequire) {
|
|
1768
|
+
provideClientReact(loaderRequire("react"));
|
|
1769
|
+
/** 测试面:浏览器用的真实定义,没有一件只为测试而存在。 */
|
|
1770
|
+
const panel = Object.freeze({
|
|
1771
|
+
interpretSnapshot,
|
|
1772
|
+
viewOf,
|
|
1773
|
+
errorOfStatus,
|
|
1774
|
+
dictionaries: Object.freeze({
|
|
1775
|
+
zh,
|
|
1776
|
+
en
|
|
1777
|
+
}),
|
|
1778
|
+
tables: Object.freeze({
|
|
1779
|
+
GUIDANCE_BY_CODE,
|
|
1780
|
+
FORM_EXCLUDED_CODES
|
|
1781
|
+
}),
|
|
1782
|
+
styles: S,
|
|
1783
|
+
helpers: Object.freeze({
|
|
1784
|
+
clockLong,
|
|
1785
|
+
count,
|
|
1786
|
+
errorText,
|
|
1787
|
+
format,
|
|
1788
|
+
isoTime,
|
|
1789
|
+
statedCadenceMs,
|
|
1790
|
+
usePollingInterval,
|
|
1791
|
+
useSnapshotPolling,
|
|
1792
|
+
getJson,
|
|
1793
|
+
postJson
|
|
1794
|
+
}),
|
|
1795
|
+
components: Object.freeze({
|
|
1796
|
+
BalanceCard,
|
|
1797
|
+
EventsList,
|
|
1798
|
+
LocalDailyCard,
|
|
1799
|
+
ModelUsageTable,
|
|
1800
|
+
PanelPage,
|
|
1801
|
+
ProviderCard,
|
|
1802
|
+
SectionCard,
|
|
1803
|
+
TokenForm,
|
|
1804
|
+
TrendBars
|
|
1805
|
+
})
|
|
1806
|
+
});
|
|
1807
|
+
return {
|
|
1808
|
+
inject,
|
|
1809
|
+
apply,
|
|
1810
|
+
panel
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
/** Host 加载的注册物:id + 工厂。 */
|
|
1814
|
+
const REGISTRATION = {
|
|
1815
|
+
id: NS,
|
|
1816
|
+
factory: clientFactory
|
|
1817
|
+
};
|
|
1818
|
+
if (typeof window !== "undefined") {
|
|
1819
|
+
const loader = window.__ModuleLoader__;
|
|
1820
|
+
if (loader !== void 0) loader.load(REGISTRATION);
|
|
1821
|
+
}
|
|
1822
|
+
if (typeof module !== "undefined" && module !== null && module.exports !== void 0) module.exports = REGISTRATION;
|
|
1823
|
+
}));
|
|
1824
|
+
|
|
1825
|
+
//#endregion
|
|
1826
|
+
return require_client();
|
|
1827
|
+
|
|
1828
|
+
})();
|