dsh-connect-modelscope-token-plan 0.2.0 → 0.3.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/client.js CHANGED
@@ -14,14 +14,32 @@ var dsh_connect_modelscope_token_plan_client = (function() {
14
14
  //#endregion
15
15
  //#region src/client/const.ts
16
16
  function modelscopeModelUrl(id) {
17
- return `${MODELSCOPE_MODEL_URL_BASE}/${encodeURI(id)}`;
17
+ return `${MODELSCOPE_MODEL_URL_BASE}/${id.split("/").map(encodeURIComponent).join("/")}`;
18
18
  }
19
19
  /** 从 `owner/model` 取 owner(无斜杠时整段作 owner)。 */
20
20
  function catalogOwner(id) {
21
21
  const slash = id.indexOf("/");
22
22
  return slash >= 0 ? id.slice(0, slash) : id;
23
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;
24
+ /**
25
+ * 徽章用的窗口短标注(tokens → "128K" / "1M")。
26
+ *
27
+ * K=1024、M=1024²,与档位值(131072…1048576)同一把尺。**不美化**:目录声明的
28
+ * 128000 显示成 "125K" 而不是 "128K"——标签必须能从数值反推出来,四舍五入到
29
+ * 好看的整数是撒谎。≥1M 用 M(`1.25M` 保留至多一位小数,去掉尾随 .0),
30
+ * ≥1024 用 K,更小的原样显示。
31
+ * @param {number} tokens - 窗口大小(tokens)。
32
+ * @returns {string} 短标注。
33
+ */
34
+ function formatWindow(tokens) {
35
+ if (tokens >= 1048576) {
36
+ const m = tokens / 1048576;
37
+ return `${m >= 10 || Number.isInteger(m) ? Math.round(m) : Math.round(m * 10) / 10}M`;
38
+ }
39
+ if (tokens >= 1024) return `${Math.round(tokens / 1024)}K`;
40
+ return String(tokens);
41
+ }
42
+ var NS, PANEL_ID, SNAPSHOT_PATH, MODELS_PATH, TOKEN_PATH, TOKEN_FORGET_PATH, PROBE_PATH, PROVIDER_PATH, PROVIDER_ROSTER_PATH, PROVIDER_CONTEXT_PATH, PROVIDER_RESET_PATH, DRAW_PATH, DRAW_RESET_PATH, HIDE_ALL_MODELS, MODELSCOPE_TOKEN_URL, MODELSCOPE_USAGE_URL, MODELSCOPE_MODEL_URL_BASE, FEATURED_OWNERS;
25
43
  var init_const = __esmMin((() => {
26
44
  NS = "dsh-connect-modelscope-token-plan";
27
45
  PANEL_ID = NS;
@@ -32,7 +50,10 @@ var dsh_connect_modelscope_token_plan_client = (function() {
32
50
  PROBE_PATH = `/api/${NS}/probe`;
33
51
  PROVIDER_PATH = `/api/${NS}/provider`;
34
52
  PROVIDER_ROSTER_PATH = `/api/${NS}/provider/roster`;
53
+ PROVIDER_CONTEXT_PATH = `/api/${NS}/provider/context`;
35
54
  PROVIDER_RESET_PATH = `/api/${NS}/provider/reset`;
55
+ DRAW_PATH = `/api/${NS}/draw`;
56
+ DRAW_RESET_PATH = `/api/${NS}/draw/reset`;
36
57
  HIDE_ALL_MODELS = "__hide_all__";
37
58
  MODELSCOPE_TOKEN_URL = "https://modelscope.cn/my/myaccesstoken";
38
59
  MODELSCOPE_USAGE_URL = "https://modelscope.cn/magicube/usage?tab=consume";
@@ -62,6 +83,10 @@ var dsh_connect_modelscope_token_plan_client = (function() {
62
83
  "panel.authError": "令牌被拒绝(401/403)。请到「接入」tab 更换访问令牌。",
63
84
  "panel.noToken": "还没有配置魔搭访问令牌。到「接入」tab 粘贴一枚 ms-… 令牌即可。",
64
85
  "panel.shapeDrift": "上游返回的结构可能有变:{detail}",
86
+ "panel.payloadError": "Host 返回了无法读取的响应(可能页面被登录墙或代理截获)。",
87
+ "shape.missingKeys": "Host 快照缺少必需的顶层键:{keys}",
88
+ "common.httpError": "无法读取 Host 响应(非预期响应,可能页面被登录墙截获)。",
89
+ "common.timeout": "请求超时,请重试。",
65
90
  "tab.quota": "额度",
66
91
  "tab.models": "模型",
67
92
  "tab.access": "接入",
@@ -81,8 +106,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
81
106
  "balance.unavailable": "魔粒余额暂不可读:{error}",
82
107
  "quota.headline": "今日 {calls} 次 · {models} 个模型",
83
108
  "quota.note": "本地口径:只统计经本插件的调用,直连魔搭的其它客户端不计入;消耗总量以官方魔粒余额为准。",
84
- "models.none": "目录暂不可读:{error}",
85
- "models.loading": "读取目录中…",
109
+ "quota.unavailable": "本地计数本次读不到(原因见上方提示)",
86
110
  "probe.validity": "验令牌(零额度)",
87
111
  "probe.validOk": "令牌有效(HTTP {status})",
88
112
  "probe.fail": "失败:{error}",
@@ -94,6 +118,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
94
118
  "provider.enable": "把魔搭模型接入 DSH 模型选择器",
95
119
  "provider.on": "已接入",
96
120
  "provider.off": "未接入",
121
+ "provider.busy": "切换中…",
97
122
  "provider.registered": "已注册(模型可在选择器里选用)",
98
123
  "provider.notRegistered": "未注册",
99
124
  "provider.llmMissing": "本机 Host 未提供 LLM 注册服务",
@@ -110,18 +135,45 @@ var dsh_connect_modelscope_token_plan_client = (function() {
110
135
  "provider.notConfigured": "还没有访问令牌,先到「接入」tab 配置",
111
136
  "provider.source.panel": "面板",
112
137
  "provider.source.config": "配置",
138
+ "provider.contextWindow": "上下文窗口",
139
+ "provider.contextDefault": "默认(跟随目录)",
140
+ "provider.contextHint": "目录没声明窗口的模型按此值生效;保存后立即重新注册",
141
+ "provider.contextCuratedTitle": "窗口来自内置策展表(models.dev 交叉真值),平台未亲口声明;带 * 标记",
142
+ "section.draw": "出图工具",
143
+ "draw.enable": "给 agent 注册 modelscope_draw_image 工具",
144
+ "draw.on": "已开启",
145
+ "draw.off": "未开启",
146
+ "draw.busy": "切换中…",
147
+ "draw.mounted": "已注册:{tool}",
148
+ "draw.notMounted": "未注册——勾选上方开关即可开启。",
149
+ "draw.restartHint": "开关立即保存,但工具的挂载发生在**下次 Host 启动**(工具注册无法撤销)。",
150
+ "draw.models": "出图将使用以下模型",
151
+ "draw.badge": "出图",
152
+ "draw.noCandidates": "当前目录里没有出图模型(平台声明 image-to-image / text-to-image 的条目为空);出图不可用。",
153
+ "draw.needsKey": "还没有访问令牌,先到「接入」tab 配置。",
154
+ "draw.reset": "回到默认",
155
+ "draw.error": "出图设置保存失败:{error}",
156
+ "draw.source.panel": "面板",
157
+ "draw.source.config": "配置",
158
+ "draw.absent.noTools": "开关已开,但本机 Host 未提供 agent tools 服务,出图工具缺席(Host 侧限制,非配置问题)。",
159
+ "draw.absent.peerLoad": "开关已开,但 tools 模块加载失败,出图工具缺席(Host 问题,非配置)。",
160
+ "draw.absent.registry": "开关已开,但工具注册被 Host 拒绝,出图工具缺席(Host 问题,非配置)。",
161
+ "draw.absent.unknown": "开关已开,但出图工具未挂载,原因未知。",
113
162
  "trend.none": "还没有本地调用记录。",
114
163
  "trend.legend": "柱长相对区间内最大值,仅本地口径。",
115
164
  "events.none": "暂无事件。",
116
165
  "events.quota": "额度",
117
166
  "events.rate_limit": "限频",
118
167
  "events.error": "错误",
168
+ "events.unknown": "未知",
119
169
  "token.status": "状态:{present}(来源 {source},校验 {valid})",
120
170
  "token.save": "保存",
121
171
  "token.forget": "忘掉已保存",
122
172
  "token.placeholder": "粘贴 ms-… 访问令牌",
173
+ "token.ariaLabel": "魔搭访问令牌",
123
174
  "token.hint": "在魔搭「访问令牌」页生成。保存即生效(写入 DSH 凭据服务,不依赖重启)。环境变量 MODELSCOPE_API_KEY 只在 DSH 启动时读一次,且优先级高于此处保存的值——设了它就别再用面板存。",
124
175
  "token.ephemeral": "此 Host 没有凭据服务:面板保存的令牌重启即丢,请改用凭据服务或环境变量。",
176
+ "token.readError": "读不到凭据存储,无法确认令牌状态(不代表没配令牌):{error}",
125
177
  "token.link": "打开魔搭访问令牌页 →",
126
178
  "source.credentials": "凭据服务",
127
179
  "source.env": "环境变量",
@@ -130,7 +182,13 @@ var dsh_connect_modelscope_token_plan_client = (function() {
130
182
  "present.yes": "已配置",
131
183
  "present.no": "未配置",
132
184
  "validity.yes": "有效",
133
- "validity.no": "未校验"
185
+ "validity.no": "未校验",
186
+ "validity.bad": "无效",
187
+ "composer.prefix": "魔搭",
188
+ "composer.pillTitle": "魔搭额度:官方魔粒余额 + 今日本地调用次数",
189
+ "composer.balanceUnit": "魔粒",
190
+ "composer.localCalls": "今日本地 {calls} 次",
191
+ "composer.stale": "上次数据"
134
192
  };
135
193
  en = {
136
194
  "panel.title": "ModelScope Access · Quota",
@@ -146,6 +204,10 @@ var dsh_connect_modelscope_token_plan_client = (function() {
146
204
  "panel.authError": "Token rejected (401/403). Replace the access token in the Access tab.",
147
205
  "panel.noToken": "No ModelScope access token configured yet. Paste an ms-… token in the Access tab.",
148
206
  "panel.shapeDrift": "Upstream shape may have changed: {detail}",
207
+ "panel.payloadError": "Host returned an unreadable response (a login wall or proxy may have intercepted it).",
208
+ "shape.missingKeys": "Host snapshot omitted required top-level key(s): {keys}",
209
+ "common.httpError": "Unexpected Host response (a login wall or proxy may have intercepted it).",
210
+ "common.timeout": "Request timed out; please retry.",
149
211
  "tab.quota": "Quota",
150
212
  "tab.models": "Models",
151
213
  "tab.access": "Access",
@@ -165,8 +227,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
165
227
  "balance.unavailable": "Magicube balance temporarily unreadable: {error}",
166
228
  "quota.headline": "Today {calls} calls · {models} models",
167
229
  "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…",
230
+ "quota.unavailable": "Local counters could not be read this time (reason in the notices above)",
170
231
  "probe.validity": "Verify token (free)",
171
232
  "probe.validOk": "Token valid (HTTP {status})",
172
233
  "probe.fail": "Failed: {error}",
@@ -178,6 +239,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
178
239
  "provider.enable": "Register ModelScope models in the DSH model picker",
179
240
  "provider.on": "On",
180
241
  "provider.off": "Off",
242
+ "provider.busy": "Switching…",
181
243
  "provider.registered": "Registered (models selectable in the picker)",
182
244
  "provider.notRegistered": "Not registered",
183
245
  "provider.llmMissing": "This Host exposes no LLM service",
@@ -194,18 +256,45 @@ var dsh_connect_modelscope_token_plan_client = (function() {
194
256
  "provider.notConfigured": "No token yet — configure it in the Access tab",
195
257
  "provider.source.panel": "panel",
196
258
  "provider.source.config": "config",
259
+ "provider.contextWindow": "Context window",
260
+ "provider.contextDefault": "Default (catalog)",
261
+ "provider.contextHint": "Applies to models whose catalog entry declares no window; re-registers immediately",
262
+ "provider.contextCuratedTitle": "Window from the built-in curated table (models.dev cross-check), not declared by the platform; marked with *",
263
+ "section.draw": "Draw tool",
264
+ "draw.enable": "Register the modelscope_draw_image tool for the agent",
265
+ "draw.on": "On",
266
+ "draw.off": "Off",
267
+ "draw.busy": "Switching…",
268
+ "draw.mounted": "Registered: {tool}",
269
+ "draw.notMounted": "Not registered — tick the switch above.",
270
+ "draw.restartHint": "The switch saves at once, but the tool mounts at the NEXT Host start (a tool registration cannot be undone).",
271
+ "draw.models": "Draw calls will use the models below",
272
+ "draw.badge": "image",
273
+ "draw.noCandidates": "This catalogue declares no image model (no entry with image-to-image / text-to-image); drawing is unavailable.",
274
+ "draw.needsKey": "No token yet — configure it in the Access tab.",
275
+ "draw.reset": "Reset",
276
+ "draw.error": "Saving the draw settings failed: {error}",
277
+ "draw.source.panel": "panel",
278
+ "draw.source.config": "config",
279
+ "draw.absent.noTools": "The switch is on, but this Host exposes no agent tools service; the draw tool is absent (a Host limitation, not config).",
280
+ "draw.absent.peerLoad": "The switch is on, but the tools module failed to load; the draw tool is absent (a Host fault, not config).",
281
+ "draw.absent.registry": "The switch is on, but the Host refused the tool registration; the draw tool is absent (a Host fault, not config).",
282
+ "draw.absent.unknown": "The switch is on, but the draw tool is not mounted; the reason is unknown.",
197
283
  "trend.none": "No local call records yet.",
198
284
  "trend.legend": "Bar lengths are relative to the maximum in the range (local scope only).",
199
285
  "events.none": "No events.",
200
286
  "events.quota": "Quota",
201
287
  "events.rate_limit": "Rate limit",
202
288
  "events.error": "Error",
289
+ "events.unknown": "Unknown",
203
290
  "token.status": "Status: {present} (source {source}, check {valid})",
204
291
  "token.save": "Save",
205
292
  "token.forget": "Forget saved",
206
293
  "token.placeholder": "Paste an ms-… access token",
294
+ "token.ariaLabel": "ModelScope access token",
207
295
  "token.hint": "Generate one on the ModelScope access-token page. Saving takes effect immediately (written to the DSH credentials service, no restart needed). The MODELSCOPE_API_KEY environment variable is read once at DSH launch and ranks ABOVE a value saved here — set it and stop using the panel.",
208
296
  "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.",
297
+ "token.readError": "Could not read the credential store, so the token state is unknown (this does NOT mean no token is configured): {error}",
209
298
  "token.link": "Open the ModelScope access-token page →",
210
299
  "source.credentials": "credentials",
211
300
  "source.env": "environment",
@@ -214,7 +303,13 @@ var dsh_connect_modelscope_token_plan_client = (function() {
214
303
  "present.yes": "configured",
215
304
  "present.no": "not configured",
216
305
  "validity.yes": "verified",
217
- "validity.no": "unverified"
306
+ "validity.no": "unverified",
307
+ "validity.bad": "invalid",
308
+ "composer.prefix": "ModelScope",
309
+ "composer.pillTitle": "ModelScope quota: official Magicube balance + today's local call count",
310
+ "composer.balanceUnit": "Magicubes",
311
+ "composer.localCalls": "local today {calls}",
312
+ "composer.stale": "stale data"
218
313
  };
219
314
  }));
220
315
 
@@ -248,7 +343,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
248
343
  if (number >= 1e4) return Math.round(number).toLocaleString();
249
344
  return String(Math.round(number * 100) / 100);
250
345
  }
251
- /** 填 `{token}` 模板。 */
346
+ /** 填 `{key}` 模板:vars 的每个键对应模板里的同名占位符。 */
252
347
  function format(template, vars) {
253
348
  let text = template;
254
349
  for (const [key, value] of Object.entries(vars || {})) text = text.split(`{${key}}`).join(String(value));
@@ -350,7 +445,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
350
445
  fontSize: 13,
351
446
  color: "var(--dsw-alias-label-secondary)",
352
447
  cursor: "pointer",
353
- outline: "none"
448
+ outlineOffset: -2
354
449
  },
355
450
  tabActive: {
356
451
  color: "var(--dsw-alias-label-primary)",
@@ -632,7 +727,43 @@ var dsh_connect_modelscope_token_plan_client = (function() {
632
727
  borderRadius: 12,
633
728
  background: "var(--dsw-alias-bg-layer-1)",
634
729
  padding: "12px 14px"
635
- }
730
+ },
731
+ pillTrigger: {
732
+ appearance: "none",
733
+ WebkitAppearance: "none",
734
+ height: 28,
735
+ borderStyle: "none",
736
+ borderWidth: 0,
737
+ background: "none",
738
+ color: "inherit",
739
+ display: "inline-flex",
740
+ alignItems: "center",
741
+ gap: 6,
742
+ padding: "0 10px",
743
+ borderRadius: 999,
744
+ fontSize: 12,
745
+ fontWeight: 600,
746
+ lineHeight: "18px",
747
+ cursor: "default",
748
+ flexShrink: 0,
749
+ whiteSpace: "nowrap"
750
+ },
751
+ pillDot: {
752
+ width: 8,
753
+ height: 8,
754
+ borderRadius: 999,
755
+ flex: "none",
756
+ background: "var(--dsw-alias-label-secondary)"
757
+ },
758
+ pillPlan: { opacity: .72 },
759
+ pillStale: {
760
+ opacity: .62,
761
+ fontStyle: "italic"
762
+ },
763
+ pillTierOk: "#22c55e",
764
+ pillTierExhausted: "#ef4444",
765
+ pillTierUnknown: "#9ca3af",
766
+ pillTierExhaustedText: { color: "var(--dsw-alias-state-error-primary)" }
636
767
  };
637
768
  }));
638
769
 
@@ -676,7 +807,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
676
807
  source: "none",
677
808
  valid: null,
678
809
  checkedAt: null,
679
- ephemeral: true
810
+ ephemeral: true,
811
+ readError: null
680
812
  };
681
813
  case "balance": return {
682
814
  available: null,
@@ -686,7 +818,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
686
818
  error: null
687
819
  };
688
820
  case "quota": return {
689
- daily: { usedLocal: 0 },
821
+ daily: { usedLocal: null },
690
822
  perModel: [],
691
823
  countingNote: "local-counting"
692
824
  };
@@ -729,17 +861,23 @@ var dsh_connect_modelscope_token_plan_client = (function() {
729
861
  */
730
862
  function interpretSnapshot(body) {
731
863
  const payload = body;
732
- if (payload && payload.ok === false) return {
864
+ if (payload && payload.ok === false) {
865
+ const code = typeof payload.code === "string" && payload.code !== "" ? payload.code : CLIENT_CODE.PAYLOAD_ERROR;
866
+ return {
867
+ data: null,
868
+ error: {
869
+ message: typeof payload.error === "string" && payload.error !== "" ? payload.error : null,
870
+ code
871
+ }
872
+ };
873
+ }
874
+ if (!payload || payload.ok !== true) return {
733
875
  data: null,
734
876
  error: {
735
- message: payload.error || "unexpected payload",
736
- code: payload.code
877
+ message: null,
878
+ code: CLIENT_CODE.PAYLOAD_ERROR
737
879
  }
738
880
  };
739
- if (!payload || payload.ok !== true) return {
740
- data: null,
741
- error: "unexpected payload"
742
- };
743
881
  const raw = payload;
744
882
  const missingKeys = SNAPSHOT_REQUIRED_KEYS.filter((key) => raw[key] === void 0);
745
883
  return {
@@ -752,19 +890,23 @@ var dsh_connect_modelscope_token_plan_client = (function() {
752
890
  * 把 wire/路由返回的 provider 块归一成 `ProviderStatus`;不合法一律降级。
753
891
  * 形状由 wire.ts 钉死,本函数只做防御性归一(旧 Host 可能缺字段)。
754
892
  */
755
- function providerOf(raw) {
893
+ function providerOf$1(raw) {
756
894
  if (raw === null || typeof raw !== "object") return DEGRADED_PROVIDER;
757
895
  const p = raw;
758
896
  const ids = Array.isArray(p.enabledIds) ? p.enabledIds.map((id) => String(id ?? "")).filter((id) => id !== "") : [];
759
897
  const roster = Array.isArray(p.roster) ? p.roster.map((entry) => {
760
898
  const row = entry ?? {};
761
899
  const id = String(row.id ?? "");
900
+ const w = row.contextWindow;
901
+ const contextSource = row.contextSource === "override" || row.contextSource === "curated" ? row.contextSource : "catalog";
762
902
  return {
763
903
  id,
764
904
  name: typeof row.name === "string" && row.name !== "" ? row.name : id,
765
905
  vision: row.vision === true,
766
906
  available: row.available !== false,
767
- quotaExhausted: row.quotaExhausted === true
907
+ quotaExhausted: row.quotaExhausted === true,
908
+ contextSource,
909
+ ...typeof w === "number" && Number.isFinite(w) && w > 0 ? { contextWindow: w } : {}
768
910
  };
769
911
  }).filter((entry) => entry.id !== "") : [];
770
912
  return {
@@ -773,10 +915,11 @@ var dsh_connect_modelscope_token_plan_client = (function() {
773
915
  llmAvailable: p.llmAvailable === true,
774
916
  registered: p.registered === true,
775
917
  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,
918
+ modelCount: typeof p.modelCount === "number" && Number.isFinite(p.modelCount) ? p.modelCount : roster.length,
919
+ enabledCount: typeof p.enabledCount === "number" && Number.isFinite(p.enabledCount) ? p.enabledCount : 0,
778
920
  allowed: p.allowed === "none" ? "none" : p.allowed === "list" ? "list" : "all",
779
921
  enabledIds: ids,
922
+ contextWindow: typeof p.contextWindow === "number" && Number.isFinite(p.contextWindow) && p.contextWindow > 0 ? p.contextWindow : null,
780
923
  roster
781
924
  };
782
925
  }
@@ -789,6 +932,33 @@ var dsh_connect_modelscope_token_plan_client = (function() {
789
932
  return `HTTP ${status}`;
790
933
  }
791
934
  /**
935
+ * 把 wire/路由返回的出图块归一成 `DrawStatus`;不合法一律降级。
936
+ *
937
+ * 与 `providerOf` 同一套防御性归一(旧 Host 可能缺字段)。`absentReason` 只认三个
938
+ * 已知码——上游若新增第四个,归一到 `null` 而不是把它透传成渲染不出的值;面板的
939
+ * 「缺席」措辞有兜底键,但**状态本身**宁可显示「未知」也不假装认识它。
940
+ * @param {unknown} raw - 原始块。
941
+ * @returns {DrawStatus} 归一后的状态。
942
+ */
943
+ function drawOf(raw) {
944
+ if (raw === null || typeof raw !== "object") return DEGRADED_DRAW;
945
+ const d = raw;
946
+ const candidates = Array.isArray(d.candidateIds) ? d.candidateIds.map((id) => String(id ?? "")).filter((id) => id !== "") : [];
947
+ const reason = d.absentReason;
948
+ const absentReason = reason === "no-tools-service" || reason === "peer-load-failed" || reason === "registry-refused" ? reason : null;
949
+ return {
950
+ enabled: d.enabled === true,
951
+ source: d.source === "panel" ? "panel" : "config",
952
+ candidateIds: candidates,
953
+ candidateCount: typeof d.candidateCount === "number" && Number.isFinite(d.candidateCount) ? d.candidateCount : candidates.length,
954
+ preferredModel: typeof d.preferredModel === "string" && d.preferredModel !== "" ? d.preferredModel : null,
955
+ preferredSource: d.preferredSource === "panel" ? "panel" : "config",
956
+ absentReason,
957
+ mountPending: d.mountPending === true,
958
+ toolName: typeof d.toolName === "string" && d.toolName !== "" ? d.toolName : "modelscope_draw_image"
959
+ };
960
+ }
961
+ /**
792
962
  * 面板决策:这张快照意味着什么。纯函数,Node 套件驱动同一个函数。
793
963
  *
794
964
  * `missingKeys` 是 `interpretSnapshot` 查出的缺失顶层键(非空 = 双端契约漂移)。
@@ -805,7 +975,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
805
975
  const guidanceKey = failure === null ? null : GUIDANCE_BY_CODE[failure.code] ?? null;
806
976
  const guidance = guidanceKey === null ? null : guidanceKey === "panel.configError" ? format(tt(guidanceKey), { error: failure?.message }) : tt(guidanceKey);
807
977
  const shapeWarnings = Array.isArray(data?.shapeWarnings) ? [...data.shapeWarnings] : [];
808
- if (missingKeys.length > 0) shapeWarnings.push(`snapshot: Host omitted required key(s): ${missingKeys.join(", ")}`);
978
+ if (missingKeys.length > 0) shapeWarnings.push(format(tt("shape.missingKeys"), { keys: missingKeys.join(", ") }));
809
979
  return {
810
980
  failure,
811
981
  needsSetup,
@@ -814,10 +984,11 @@ var dsh_connect_modelscope_token_plan_client = (function() {
814
984
  shapeWarnings
815
985
  };
816
986
  }
817
- var DEGRADED_PROVIDER, GUIDANCE_BY_CODE, FORM_EXCLUDED_CODES;
987
+ var CLIENT_CODE, DEGRADED_PROVIDER, DEGRADED_DRAW, GUIDANCE_BY_CODE, FORM_EXCLUDED_CODES;
818
988
  var init_snapshot = __esmMin((() => {
819
989
  init_format();
820
990
  init_wire();
991
+ CLIENT_CODE = Object.freeze({ PAYLOAD_ERROR: "payload_error" });
821
992
  DEGRADED_PROVIDER = Object.freeze({
822
993
  enabled: false,
823
994
  source: "config",
@@ -828,8 +999,20 @@ var dsh_connect_modelscope_token_plan_client = (function() {
828
999
  enabledCount: 0,
829
1000
  allowed: "all",
830
1001
  enabledIds: [],
1002
+ contextWindow: null,
831
1003
  roster: []
832
1004
  });
1005
+ DEGRADED_DRAW = Object.freeze({
1006
+ enabled: false,
1007
+ source: "config",
1008
+ candidateIds: [],
1009
+ candidateCount: 0,
1010
+ preferredModel: null,
1011
+ preferredSource: "config",
1012
+ absentReason: null,
1013
+ mountPending: false,
1014
+ toolName: "modelscope_draw_image"
1015
+ });
833
1016
  GUIDANCE_BY_CODE = Object.freeze({
834
1017
  auth_error: "panel.authError",
835
1018
  config_error: "panel.configError",
@@ -838,7 +1021,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
838
1021
  upstream_error: "panel.upstream",
839
1022
  rate_limited: "panel.upstream",
840
1023
  quota_exceeded: "panel.upstream",
841
- internal_error: "panel.internalError"
1024
+ internal_error: "panel.internalError",
1025
+ payload_error: "panel.payloadError"
842
1026
  });
843
1027
  FORM_EXCLUDED_CODES = Object.freeze(/* @__PURE__ */ new Set([
844
1028
  "config_error",
@@ -847,7 +1031,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
847
1031
  "upstream_error",
848
1032
  "rate_limited",
849
1033
  "quota_exceeded",
850
- "internal_error"
1034
+ "internal_error",
1035
+ "payload_error"
851
1036
  ]));
852
1037
  }));
853
1038
 
@@ -899,8 +1084,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
899
1084
  inset: 0,
900
1085
  borderRadius: 999,
901
1086
  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)",
1087
+ border: `1px solid ${on ? BRAND : "var(--dsw-alias-border-l2, #36373b)"}`,
1088
+ background: on ? BRAND : "var(--dsw-alias-bg-layer-2, #2a2b31)",
904
1089
  transition: "background .15s, border-color .15s"
905
1090
  }
906
1091
  }, h("span", { style: {
@@ -921,6 +1106,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
921
1106
  var TRACK_W, TRACK_H, THUMB, TRAVEL;
922
1107
  var init_toggle_switch = __esmMin((() => {
923
1108
  init_runtime();
1109
+ init_styles();
924
1110
  TRACK_W = 30;
925
1111
  TRACK_H = 17;
926
1112
  THUMB = 12;
@@ -982,12 +1168,17 @@ var dsh_connect_modelscope_token_plan_client = (function() {
982
1168
  */
983
1169
  function LocalDailyCard({ snapshot, tt }) {
984
1170
  const quota = snapshot.quota;
985
- const daily = quota?.daily ?? { usedLocal: 0 };
1171
+ const daily = quota?.daily ?? { usedLocal: null };
986
1172
  const perModel = Array.isArray(quota?.perModel) ? quota.perModel : [];
1173
+ const unknownUsed = !(typeof daily.usedLocal === "number" && Number.isFinite(daily.usedLocal));
1174
+ const usedText = unknownUsed ? "—" : count(daily.usedLocal);
987
1175
  return h("div", { style: S.card }, h("div", { style: S.poolName }, format(tt("quota.headline"), {
988
- calls: count(daily.usedLocal),
1176
+ calls: usedText,
989
1177
  models: count(perModel.length)
990
- })), h(ModelUsageTable, {
1178
+ })), unknownUsed ? h("div", { style: {
1179
+ ...S.formNote,
1180
+ color: "var(--dsw-alias-state-warn-primary)"
1181
+ } }, tt("quota.unavailable")) : null, h(ModelUsageTable, {
991
1182
  rows: perModel,
992
1183
  tt
993
1184
  }), h("div", { style: S.trendLegend }, tt("quota.note")));
@@ -1019,21 +1210,25 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1019
1210
  /** 近 N 天本地调用趋势(天桶;缺桶 = 该日 0 次,真实零)。 */
1020
1211
  function TrendBars({ buckets, tt }) {
1021
1212
  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,
1213
+ const rows = buckets.map((bucket) => ({
1214
+ dateKey: bucket.dateKey,
1215
+ calls: Number.isFinite(bucket.calls) ? Math.max(0, bucket.calls) : 0
1216
+ }));
1217
+ const max = Math.max(0, ...rows.map((row) => row.calls));
1218
+ return h("div", null, rows.map((row) => h("div", {
1219
+ key: row.dateKey,
1025
1220
  style: S.trendRowHead
1026
- }, h("span", { style: S.trendModel }, bucket.dateKey), h("div", {
1221
+ }, h("span", { style: S.trendModel }, row.dateKey), h("div", {
1027
1222
  style: {
1028
1223
  ...S.trendBar,
1029
1224
  flex: "1",
1030
1225
  marginLeft: 10
1031
1226
  },
1032
1227
  "aria-hidden": "true"
1033
- }, max > 0 && bucket.calls > 0 ? h("div", { style: {
1228
+ }, max > 0 && row.calls > 0 ? h("div", { style: {
1034
1229
  ...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")));
1230
+ width: `${row.calls / max * 100}%`
1231
+ } }) : null), h("span", { style: S.quotaUsed }, String(row.calls)))), h("div", { style: S.trendLegend }, tt("trend.legend")));
1037
1232
  }
1038
1233
  /** 事件流(429/错误),Newest-first。 */
1039
1234
  function EventsList({ events, tt }) {
@@ -1041,7 +1236,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1041
1236
  return h("div", null, events.map((event, index) => h("div", {
1042
1237
  key: `${event.at}-${index}`,
1043
1238
  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", {
1239
+ }, h("span", { style: S.quotaUsed }, isoTime(event.at)), h("span", { style: S.modelTag }, tt(EVENT_KIND_KEY[event.kind] ?? "events.unknown")), h("span", {
1045
1240
  style: {
1046
1241
  ...S.trendModel,
1047
1242
  flex: "1"
@@ -1054,8 +1249,11 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1054
1249
  const [value, setValue] = useState("");
1055
1250
  const source = token?.source ?? "none";
1056
1251
  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"), {
1252
+ const validKey = token?.valid === true ? "validity.yes" : token?.valid === false ? "validity.bad" : "validity.no";
1253
+ return h("div", null, token?.readError ? h("div", { style: {
1254
+ ...S.formNote,
1255
+ color: "var(--dsw-alias-state-warn-primary)"
1256
+ } }, format(tt("token.readError"), { error: token.readError })) : h("div", { style: S.quotaUsed }, format(tt("token.status"), {
1059
1257
  present: token?.present ? tt("present.yes") : tt("present.no"),
1060
1258
  source: tt(sourceKey),
1061
1259
  valid: tt(validKey)
@@ -1063,6 +1261,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1063
1261
  style: S.input,
1064
1262
  type: "password",
1065
1263
  placeholder: tt("token.placeholder"),
1264
+ "aria-label": tt("token.ariaLabel"),
1066
1265
  value,
1067
1266
  onChange: (event) => setValue(String(event.target.value ?? ""))
1068
1267
  }), h("button", {
@@ -1109,25 +1308,30 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1109
1308
  *
1110
1309
  * 允许清单的语义(路由 §9 钉死):空清单 = 不过滤 = 全部提供,所以「全部」
1111
1310
  * 发 `[]`;「全部隐藏」发哨兵 `HIDE_ALL_MODELS`(「什么都不提供」)。哨兵
1112
- * 不是任何真实模型 id,所以同一套 `draft.includes(id)` 的勾选判断天然不会把
1113
- * 它画成勾中——不需要特判。
1311
+ * 是**独占态**:draft 含哨兵时整个清单就是「全部隐藏」——勾一个真实模型会
1312
+ * 退出哨兵态(剔除哨兵),Host 落盘值带哨兵时只保留哨兵。**不能**让哨兵与
1313
+ * 真实 id 混排:Host 的 resolveAllowedList 判「含哨兵即 none(全隐藏)」,而
1314
+ * 面板若把混排清单里的真实 id 画成勾中、计数为 N,UI 就与落盘撒谎了。
1315
+ * 曾踩过:点「全部隐藏」后复选框仍可勾,勾出的混排清单保存后「已启用 1/N」
1316
+ * 而实际提供 0 个。
1114
1317
  *
1115
1318
  * 额度耗尽的模型保留在清单里(灰显、勾选框禁用),这是故意的:roster 是目录
1116
1319
  * 事实,picker 那边由 Host 自己丢掉它们,面板不替它删。
1117
1320
  */
1118
- function ProviderCard({ provider, busy, error, onToggle, onSaveList, onReset, tokenPresent, tt }) {
1119
- const status = providerOf(provider);
1321
+ function ProviderCard({ provider, busy, error, onToggle, onSaveList, onSaveContextWindow, onReset, tokenPresent, tt }) {
1322
+ const status = providerOf$1(provider);
1120
1323
  const enabled = status.enabled === true;
1121
1324
  const hostIds = status.enabledIds;
1122
1325
  const hostKey = useMemo(() => JSON.stringify(hostIds), [hostIds]);
1123
1326
  const [draft, setDraft] = useState(() => hostIds.slice());
1327
+ const hideAllMode = draft.includes(HIDE_ALL_MODELS);
1124
1328
  useEffect(() => {
1125
- setDraft(hostIds);
1329
+ setDraft(hostIds.includes("__hide_all__") ? [HIDE_ALL_MODELS] : hostIds);
1126
1330
  }, [hostKey]);
1127
1331
  const roster = status.roster;
1128
1332
  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;
1333
+ const toggleOne = (id) => setDraft((current) => current.includes(id) ? current.filter((x) => x !== id) : [...current.filter((x) => x !== HIDE_ALL_MODELS), id]);
1334
+ const ticked = hideAllMode ? 0 : roster.filter((row) => draft.includes(row.id)).length;
1131
1335
  const degraded = status.error === "unavailable";
1132
1336
  let statusNode;
1133
1337
  if (!degraded && typeof status.error === "string" && status.error !== "") statusNode = h("div", {
@@ -1157,6 +1361,61 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1157
1361
  color: "var(--dsw-alias-state-warn-primary)"
1158
1362
  } }, tt("provider.notConfigured")) : null;
1159
1363
  const sourceKey = status.source === "panel" ? "provider.source.panel" : "provider.source.config";
1364
+ const CONTEXT_TIERS = [
1365
+ {
1366
+ value: 131072,
1367
+ label: "128K"
1368
+ },
1369
+ {
1370
+ value: 262144,
1371
+ label: "256K"
1372
+ },
1373
+ {
1374
+ value: 524288,
1375
+ label: "512K"
1376
+ },
1377
+ {
1378
+ value: 1048576,
1379
+ label: "1M"
1380
+ }
1381
+ ];
1382
+ const contextNode = onSaveContextWindow === void 0 ? null : h("div", { style: {
1383
+ display: "flex",
1384
+ gap: 8,
1385
+ alignItems: "center",
1386
+ flexWrap: "wrap",
1387
+ margin: "0 0 8px"
1388
+ } }, h("label", {
1389
+ style: {
1390
+ fontSize: 12,
1391
+ color: "var(--dsw-alias-label-secondary)"
1392
+ },
1393
+ htmlFor: "provider-context-window"
1394
+ }, tt("provider.contextWindow")), h("select", {
1395
+ id: "provider-context-window",
1396
+ style: {
1397
+ ...S.rosterBulk,
1398
+ width: "auto"
1399
+ },
1400
+ disabled: busy,
1401
+ value: status.contextWindow === null ? "" : String(status.contextWindow),
1402
+ onChange: (event) => {
1403
+ const value = String(event.target?.value ?? "");
1404
+ onSaveContextWindow(value === "" ? null : Number(value));
1405
+ }
1406
+ }, h("option", {
1407
+ value: "",
1408
+ key: "default"
1409
+ }, tt("provider.contextDefault")), CONTEXT_TIERS.map((tier) => h("option", {
1410
+ value: String(tier.value),
1411
+ key: String(tier.value)
1412
+ }, tier.label))), h("span", {
1413
+ style: {
1414
+ ...S.muted,
1415
+ fontSize: 11
1416
+ },
1417
+ title: tt("provider.contextHint")
1418
+ }, tt("provider.contextHint")));
1160
1419
  const switchRow = h("div", { style: {
1161
1420
  display: "flex",
1162
1421
  gap: 10,
@@ -1168,6 +1427,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1168
1427
  onChange: () => onToggle(!enabled),
1169
1428
  busy,
1170
1429
  label: tt(enabled ? "provider.on" : "provider.off"),
1430
+ busyLabel: tt("provider.busy"),
1171
1431
  title: tt("provider.enable")
1172
1432
  }), h("span", { style: {
1173
1433
  fontSize: 11,
@@ -1228,7 +1488,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1228
1488
  cursor: busy || unusable ? "default" : "pointer"
1229
1489
  } }, h("input", {
1230
1490
  type: "checkbox",
1231
- checked: draft.includes(id),
1491
+ checked: !hideAllMode && draft.includes(id),
1232
1492
  disabled: busy || unusable,
1233
1493
  onChange: () => toggleOne(id),
1234
1494
  style: S.modelCheck,
@@ -1236,7 +1496,13 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1236
1496
  }), h("span", {
1237
1497
  style: S.modelName,
1238
1498
  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", {
1499
+ }, row.name)), row.vision === true ? h("span", { style: S.modelBadge }, tt("provider.vision")) : null, typeof row.contextWindow === "number" ? h("span", {
1500
+ style: row.contextSource === "curated" ? {
1501
+ ...S.modelBadge,
1502
+ color: "var(--dsw-alias-label-secondary)"
1503
+ } : S.modelBadge,
1504
+ title: row.contextSource === "curated" ? tt("provider.contextCuratedTitle") : tt("provider.contextWindow")
1505
+ }, row.contextSource === "curated" ? formatWindow(row.contextWindow) + " *" : formatWindow(row.contextWindow)) : null, FEATURED_OWNERS.includes(catalogOwner(row.id)) ? h("span", {
1240
1506
  style: S.modelBadgeFeatured,
1241
1507
  title: tt("models.featuredTitle")
1242
1508
  }, tt("provider.featured")) : null, row.quotaExhausted === true ? h("span", { style: {
@@ -1259,7 +1525,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1259
1525
  disabled: busy,
1260
1526
  onClick: onReset
1261
1527
  }, tt("provider.reset"))));
1262
- return h("div", null, switchRow, statusNode, tokenHint, h("div", { style: {
1528
+ return h("div", null, switchRow, statusNode, tokenHint, contextNode, h("div", { style: {
1263
1529
  ...S.sectionTitle,
1264
1530
  margin: "18px 0 8px"
1265
1531
  } }, tt("provider.models")), rosterBlock, error !== null ? h("div", {
@@ -1267,7 +1533,114 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1267
1533
  role: "alert"
1268
1534
  }, error) : null);
1269
1535
  }
1270
- var EVENT_KIND_KEY;
1536
+ /**
1537
+ * 出图工具卡:开关、候选模型、挂载状态。
1538
+ *
1539
+ * **与 ProviderCard 的关键差异,也是这张卡存在的主要理由**:provider 开关翻转会
1540
+ * 立即重注册,而出图工具的开关翻转**要等下次 Host 启动**——peer 的 tools registry
1541
+ * 没有 unregister。所以这张卡必须把这件事说出来(`draw.restartHint`),否则用户
1542
+ * 翻开关后看不到任何变化,会以为坏了。
1543
+ *
1544
+ * 缺席原因是**分开渲染**的:`no-tools-service` 是「这个 Host 没提供该服务」(正常),
1545
+ * 另两种是 Host 缺陷。三种都给出各自的一句话,因为「开关开着但工具不在」如果没有
1546
+ * 解释,用户没有任何办法自己诊断。
1547
+ * @param {object} props - 渲染参数。
1548
+ * @returns {unknown} 卡片节点。
1549
+ */
1550
+ function DrawCard({ draw, busy, error, onToggle, onReset, tokenPresent, tt }) {
1551
+ const status = drawOf(draw);
1552
+ const enabled = status.enabled === true;
1553
+ const candidates = status.candidateIds;
1554
+ const sourceKey = status.source === "panel" ? "draw.source.panel" : "draw.source.config";
1555
+ const switchRow = h("div", { style: {
1556
+ display: "flex",
1557
+ gap: 10,
1558
+ alignItems: "center",
1559
+ flexWrap: "wrap",
1560
+ margin: "0 0 8px"
1561
+ } }, h(ToggleSwitch, {
1562
+ checked: enabled,
1563
+ onChange: () => onToggle(!enabled),
1564
+ busy,
1565
+ label: tt(enabled ? "draw.on" : "draw.off"),
1566
+ busyLabel: tt("draw.busy"),
1567
+ title: tt("draw.enable")
1568
+ }), h("span", { style: {
1569
+ fontSize: 11,
1570
+ color: "var(--dsw-alias-label-secondary)"
1571
+ } }, tt(sourceKey)));
1572
+ let statusNode;
1573
+ if (status.absentReason !== null) {
1574
+ const key = DRAW_ABSENT_KEY[status.absentReason] ?? "draw.absent.unknown";
1575
+ const severe = status.absentReason !== "no-tools-service";
1576
+ statusNode = h("div", {
1577
+ style: severe ? {
1578
+ ...S.formNote,
1579
+ color: "var(--dsw-alias-state-warn-primary)"
1580
+ } : S.formNote,
1581
+ role: "status"
1582
+ }, tt(key));
1583
+ } else if (enabled) statusNode = h("div", {
1584
+ style: {
1585
+ fontSize: 12,
1586
+ color: "var(--dsw-alias-label-secondary)"
1587
+ },
1588
+ role: "status"
1589
+ }, format(tt("draw.mounted"), { tool: status.toolName }));
1590
+ else statusNode = h("div", {
1591
+ style: {
1592
+ ...S.muted,
1593
+ fontSize: 12
1594
+ },
1595
+ role: "status"
1596
+ }, tt("draw.notMounted"));
1597
+ const restartHint = enabled ? h("div", { style: {
1598
+ ...S.formNote,
1599
+ color: "var(--dsw-alias-state-warn-primary)"
1600
+ } }, tt("draw.restartHint")) : null;
1601
+ const tokenHint = enabled && tokenPresent === false ? h("div", { style: {
1602
+ ...S.formNote,
1603
+ color: "var(--dsw-alias-state-warn-primary)"
1604
+ } }, tt("draw.needsKey")) : null;
1605
+ const candidateBlock = candidates.length === 0 ? h("div", { style: {
1606
+ ...S.muted,
1607
+ fontSize: 12,
1608
+ marginTop: 6
1609
+ } }, tt("draw.noCandidates")) : h("div", { style: {
1610
+ display: "flex",
1611
+ flexDirection: "column",
1612
+ gap: 4
1613
+ } }, candidates.map((id) => h("div", {
1614
+ key: id,
1615
+ style: S.modelRowHead
1616
+ }, h("code", {
1617
+ style: S.modelName,
1618
+ title: id
1619
+ }, id), h("span", { style: S.modelBadge }, tt("draw.badge")), h("a", {
1620
+ style: {
1621
+ ...S.formNote,
1622
+ margin: 0,
1623
+ marginLeft: "auto"
1624
+ },
1625
+ href: modelscopeModelUrl(id),
1626
+ target: "_blank",
1627
+ rel: "noreferrer",
1628
+ title: tt("models.viewOnSiteTitle")
1629
+ }, tt("models.viewOnSite")))));
1630
+ return h("div", null, switchRow, statusNode, restartHint, tokenHint, h("div", { style: {
1631
+ ...S.sectionTitle,
1632
+ margin: "18px 0 8px"
1633
+ } }, tt("draw.models")), candidateBlock, h("div", { style: S.rosterFoot }, h("button", {
1634
+ type: "button",
1635
+ style: S.button,
1636
+ disabled: busy,
1637
+ onClick: onReset
1638
+ }, tt("draw.reset"))), error !== null ? h("div", {
1639
+ style: S.formError,
1640
+ role: "alert"
1641
+ }, error) : null);
1642
+ }
1643
+ var EVENT_KIND_KEY, DRAW_ABSENT_KEY;
1271
1644
  var init_cards = __esmMin((() => {
1272
1645
  init_format();
1273
1646
  init_runtime();
@@ -1280,13 +1653,38 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1280
1653
  rate_limit: "events.rate_limit",
1281
1654
  error: "events.error"
1282
1655
  };
1656
+ DRAW_ABSENT_KEY = {
1657
+ "no-tools-service": "draw.absent.noTools",
1658
+ "peer-load-failed": "draw.absent.peerLoad",
1659
+ "registry-refused": "draw.absent.registry"
1660
+ };
1283
1661
  }));
1284
1662
 
1285
1663
  //#endregion
1286
1664
  //#region src/client/http.ts
1287
- /** POST 并解析 body;非 JSON 响应返回 null。 */
1665
+ /** 一次带超时的 fetch;内部 controller,调用方无需传 signal。 */
1666
+ async function fetchWithTimeout(url, init) {
1667
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
1668
+ const timer = controller === null ? null : setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
1669
+ try {
1670
+ return await fetch(url, {
1671
+ ...init,
1672
+ ...controller === null ? {} : { signal: controller.signal }
1673
+ });
1674
+ } finally {
1675
+ if (timer !== null) clearTimeout(timer);
1676
+ }
1677
+ }
1678
+ /**
1679
+ * POST 并解析 body;非 JSON 响应返回 null。
1680
+ *
1681
+ * 这里**故意不**检查 `response.ok`(与 {@link getJson} 不同):host 的写操作错误
1682
+ * 响应体是 `{ok:false, code, error}`,调用方靠 `body.error` 给用户看具体原因
1683
+ * (令牌格式错、额度耗尽、上游超时)。丢掉它就只能显示一句通用的「操作失败」,
1684
+ * 用户无从下手。getJson 可以直接返回 null——读操作没有错误详情可展示。
1685
+ */
1288
1686
  async function postJson(path, payload) {
1289
- return await (await fetch(path, {
1687
+ return await (await fetchWithTimeout(path, {
1290
1688
  method: "POST",
1291
1689
  headers: {
1292
1690
  "content-type": "application/json",
@@ -1298,14 +1696,35 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1298
1696
  }
1299
1697
  /** GET 同源 JSON;非 2xx 或非 JSON 返回 null(调用方决定怎么降级)。 */
1300
1698
  async function getJson(path) {
1301
- const response = await fetch(path, {
1699
+ const response = await fetchWithTimeout(path, {
1302
1700
  headers: { accept: "application/json" },
1303
1701
  cache: "no-store"
1304
1702
  });
1305
1703
  if (!response.ok) return null;
1306
1704
  return await response.json().catch(() => null);
1307
1705
  }
1308
- var init_http = __esmMin((() => {}));
1706
+ /**
1707
+ * 写路径的成败判定收口:失败返回 Error,成功返回 null。
1708
+ *
1709
+ * `postJson` 只把「HTTP/解析失败」折成 `null`,域失败是 `{ok:false, error}`(Host 对
1710
+ * 写操作恒回 HTTP 200,成败看 body)。三个写回调曾各判一次,而**「忘掉令牌」漏判**——
1711
+ * 它直接 `await postJson(...)` 就往下走,于是 Host 侧刻意传播的失败(凭据文件只读时
1712
+ * `ms-auth.forget` 抛错,见其文件头「谎报成功比失败更糟」)在界面上表现为「什么都没
1713
+ * 发生」:没有错误行、busy 清掉、刷新后的快照里令牌还在。判据收在这里一份,三个调用
1714
+ * 方(开关/清单、保存令牌、忘掉令牌)不可能再各自漂移。
1715
+ * @param {ApiBody|null} body - postJson 的返回值。
1716
+ * @param {string} fallback - body 里没有可读原因时的一句话。
1717
+ * @returns {Error|null} 失败原因;`null` = 成功。
1718
+ */
1719
+ function writeFailure(body, fallback) {
1720
+ if (body !== null && body.ok === true) return null;
1721
+ const reason = typeof body?.error === "string" && body.error.trim() !== "" ? body.error : fallback;
1722
+ return new Error(reason);
1723
+ }
1724
+ var FETCH_TIMEOUT_MS;
1725
+ var init_http = __esmMin((() => {
1726
+ FETCH_TIMEOUT_MS = 2e4;
1727
+ }));
1309
1728
 
1310
1729
  //#endregion
1311
1730
  //#region src/client/use-polling-interval.ts
@@ -1382,12 +1801,19 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1382
1801
  const [cadenceMs, setCadenceMs] = useState(defaultCadenceMs);
1383
1802
  /** Host 少给的顶层键(契约漂移);透给面板当 shapeWarning,不静默。 */
1384
1803
  const [missingKeys, setMissingKeys] = useState([]);
1804
+ /**
1805
+ * 每一次轮询尝试都加一,成功与失败都算。`updatedAt` 只在成功时变,而
1806
+ * 面板的 provider 兜底 GET 需要的是「下一轮还会再来」这个信号——快照本身
1807
+ * 失败的那一轮,恰好是兜底路径也应该再试的一轮。
1808
+ */
1809
+ const [pollRevision, setPollRevision] = useState(0);
1385
1810
  const generation = useRef(0);
1386
1811
  const inFlight = useRef(null);
1387
1812
  const cadenceRef = useRef(cadenceMs);
1388
1813
  cadenceRef.current = cadenceMs;
1389
1814
  const load = useCallback(async () => {
1390
1815
  generation.current += 1;
1816
+ setPollRevision((revision) => revision + 1);
1391
1817
  const mine = generation.current;
1392
1818
  const isCurrent = () => generation.current === mine;
1393
1819
  inFlight.current?.abort?.();
@@ -1438,7 +1864,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1438
1864
  loadedOnce,
1439
1865
  updatedAt,
1440
1866
  load,
1441
- missingKeys
1867
+ missingKeys,
1868
+ pollRevision
1442
1869
  };
1443
1870
  }
1444
1871
  var init_use_snapshot_polling = __esmMin((() => {
@@ -1452,7 +1879,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1452
1879
  //#endregion
1453
1880
  //#region src/client/panel-page.ts
1454
1881
  function PanelPage({ tt, localeSubscribe }) {
1455
- const { data, error, loadedOnce, updatedAt, load, missingKeys } = useSnapshotPolling();
1882
+ const { data, error, updatedAt, load, missingKeys, pollRevision } = useSnapshotPolling();
1456
1883
  const [, setLocaleRevision] = useState(0);
1457
1884
  const [openSections, setOpenSections] = useState({
1458
1885
  balance: true,
@@ -1460,6 +1887,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1460
1887
  trend: true,
1461
1888
  events: false,
1462
1889
  provider: true,
1890
+ draw: true,
1463
1891
  token: true
1464
1892
  });
1465
1893
  const [activeTab, setActiveTab] = useState("quota");
@@ -1473,67 +1901,126 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1473
1901
  [key]: !current[key]
1474
1902
  }));
1475
1903
  }, []);
1476
- const { failure, needsSetup, guidance, shapeWarnings } = viewOf(data, error, tt, missingKeys);
1477
- const showSetup = needsSetup && loadedOnce;
1904
+ const { failure, guidance, shapeWarnings } = viewOf(data, error, tt, missingKeys);
1478
1905
  const rawProvider = data?.provider;
1479
1906
  const hasProviderBlock = rawProvider !== void 0 && rawProvider !== null;
1480
1907
  const [fetchedProvider, setFetchedProvider] = useState(null);
1481
- const provider = hasProviderBlock ? providerOf(rawProvider) : fetchedProvider ?? DEGRADED_PROVIDER;
1908
+ const provider = hasProviderBlock ? providerOf$1(rawProvider) : fetchedProvider ?? DEGRADED_PROVIDER;
1482
1909
  useEffect(() => {
1483
1910
  if (hasProviderBlock) return void 0;
1484
1911
  let cancelled = false;
1485
1912
  getJson(PROVIDER_PATH).then((body) => {
1486
- if (!cancelled && body !== null && body.ok === true) setFetchedProvider(providerOf(body));
1913
+ if (!cancelled && body !== null && body.ok === true) setFetchedProvider(providerOf$1(body));
1914
+ }).catch(() => {});
1915
+ return () => {
1916
+ cancelled = true;
1917
+ };
1918
+ }, [hasProviderBlock, pollRevision]);
1919
+ const rawDraw = data?.draw;
1920
+ const hasDrawBlock = rawDraw !== void 0 && rawDraw !== null;
1921
+ const [fetchedDraw, setFetchedDraw] = useState(null);
1922
+ const draw = hasDrawBlock ? drawOf(rawDraw) : fetchedDraw ?? DEGRADED_DRAW;
1923
+ useEffect(() => {
1924
+ if (hasDrawBlock) return void 0;
1925
+ let cancelled = false;
1926
+ getJson(DRAW_PATH).then((body) => {
1927
+ if (!cancelled && body !== null && body.ok === true) setFetchedDraw(drawOf(body));
1487
1928
  }).catch(() => {});
1488
1929
  return () => {
1489
1930
  cancelled = true;
1490
1931
  };
1491
- }, [hasProviderBlock]);
1932
+ }, [hasDrawBlock, pollRevision]);
1492
1933
  const [providerBusy, setProviderBusy] = useState(false);
1493
1934
  const [providerError, setProviderError] = useState(null);
1935
+ const writeErrorText = (reason, t) => reason instanceof Error && reason.name === "AbortError" ? t("common.timeout") : errorText(reason);
1936
+ /**
1937
+ * POST 一个写路径,并把 body 里的域失败抛出去(Host 对写操作恒回 HTTP 200,成败
1938
+ * 看 body)。判据只有 `http.ts#writeFailure` 一份——三个写回调曾各判一次,而
1939
+ * 「忘掉令牌」那条漏了,于是凭据文件只读时界面表现为「什么都没发生」。
1940
+ */
1941
+ const postWrite = useCallback(async (path, payload) => {
1942
+ const failure = writeFailure(await postJson(path, payload), tt("common.httpError"));
1943
+ if (failure !== null) throw failure;
1944
+ }, [tt]);
1494
1945
  const runProviderWrite = useCallback(async (path, payload) => {
1495
1946
  setProviderBusy(true);
1496
1947
  setProviderError(null);
1497
1948
  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");
1949
+ await postWrite(path, payload);
1500
1950
  load();
1501
1951
  } catch (reason) {
1502
- setProviderError(format(tt("provider.error"), { error: errorText(reason) }));
1952
+ setProviderError(format(tt("provider.error"), { error: writeErrorText(reason, tt) }));
1503
1953
  } finally {
1504
1954
  setProviderBusy(false);
1505
1955
  }
1506
- }, [load, tt]);
1956
+ }, [
1957
+ load,
1958
+ postWrite,
1959
+ tt
1960
+ ]);
1507
1961
  const toggleProvider = useCallback((enabled) => void runProviderWrite(PROVIDER_PATH, { enabled }), [runProviderWrite]);
1508
- const saveRoster = useCallback((enabledIds) => void runProviderWrite(PROVIDER_ROSTER_PATH, { enabledIds }), [runProviderWrite]);
1962
+ const saveRoster = useCallback((enabledIds) => {
1963
+ const normalized = enabledIds.includes("__hide_all__") ? [HIDE_ALL_MODELS] : enabledIds.filter((id) => id !== HIDE_ALL_MODELS);
1964
+ runProviderWrite(PROVIDER_ROSTER_PATH, { enabledIds: normalized });
1965
+ }, [runProviderWrite]);
1509
1966
  const resetProvider = useCallback(() => void runProviderWrite(PROVIDER_RESET_PATH, {}), [runProviderWrite]);
1967
+ const saveContextWindow = useCallback((contextWindow) => {
1968
+ runProviderWrite(PROVIDER_CONTEXT_PATH, { contextWindow });
1969
+ }, [runProviderWrite]);
1970
+ const [drawBusy, setDrawBusy] = useState(false);
1971
+ const [drawError, setDrawError] = useState(null);
1972
+ const runDrawWrite = useCallback(async (path, payload) => {
1973
+ setDrawBusy(true);
1974
+ setDrawError(null);
1975
+ try {
1976
+ await postWrite(path, payload);
1977
+ load();
1978
+ } catch (reason) {
1979
+ setDrawError(format(tt("draw.error"), { error: writeErrorText(reason, tt) }));
1980
+ } finally {
1981
+ setDrawBusy(false);
1982
+ }
1983
+ }, [
1984
+ load,
1985
+ postWrite,
1986
+ tt
1987
+ ]);
1988
+ const toggleDraw = useCallback((enabled) => void runDrawWrite(DRAW_PATH, { enabled }), [runDrawWrite]);
1989
+ const resetDraw = useCallback(() => void runDrawWrite(DRAW_RESET_PATH, {}), [runDrawWrite]);
1510
1990
  const [tokenBusy, setTokenBusy] = useState(false);
1511
1991
  const [tokenError, setTokenError] = useState(null);
1512
1992
  const saveToken = useCallback(async (value) => {
1513
1993
  setTokenBusy(true);
1514
1994
  setTokenError(null);
1515
1995
  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");
1996
+ await postWrite(TOKEN_PATH, { token: value });
1518
1997
  load();
1519
1998
  } catch (reason) {
1520
- setTokenError(errorText(reason));
1999
+ setTokenError(writeErrorText(reason, tt));
1521
2000
  } finally {
1522
2001
  setTokenBusy(false);
1523
2002
  }
1524
- }, [load]);
2003
+ }, [
2004
+ load,
2005
+ postWrite,
2006
+ tt
2007
+ ]);
1525
2008
  const forgetToken = useCallback(async () => {
1526
2009
  setTokenBusy(true);
1527
2010
  setTokenError(null);
1528
2011
  try {
1529
- await postJson(TOKEN_FORGET_PATH, {});
2012
+ await postWrite(TOKEN_FORGET_PATH, {});
1530
2013
  load();
1531
2014
  } catch (reason) {
1532
- setTokenError(errorText(reason));
2015
+ setTokenError(writeErrorText(reason, tt));
1533
2016
  } finally {
1534
2017
  setTokenBusy(false);
1535
2018
  }
1536
- }, [load]);
2019
+ }, [
2020
+ load,
2021
+ postWrite,
2022
+ tt
2023
+ ]);
1537
2024
  const tokenState = data?.token ?? null;
1538
2025
  const sampleModel = data?.models?.sample?.[0] ?? null;
1539
2026
  const [verifyBusy, setVerifyBusy] = useState(false);
@@ -1545,20 +2032,17 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1545
2032
  setVerifyError(null);
1546
2033
  try {
1547
2034
  const body = await postJson(PROBE_PATH, { modelId });
2035
+ if (body !== null) load();
1548
2036
  if (body !== null && body.ok === true) setVerifyNote(format(tt("probe.validOk"), { status: Number(body.status ?? 0) }));
1549
- else setVerifyError(typeof body?.error === "string" ? body.error : "HTTP error");
2037
+ else setVerifyError(typeof body?.error === "string" ? body.error : tt("common.httpError"));
1550
2038
  } catch (reason) {
1551
- setVerifyError(errorText(reason));
2039
+ setVerifyError(writeErrorText(reason, tt));
1552
2040
  } finally {
1553
2041
  setVerifyBusy(false);
1554
2042
  }
1555
- }, [tt]);
2043
+ }, [load, tt]);
1556
2044
  const quotaBody = () => {
1557
2045
  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 })));
1558
- if (showSetup && failure !== null) return h("div", {
1559
- style: S.empty,
1560
- role: "alert"
1561
- }, guidance ?? format(tt("panel.error"), { error: failure.message }));
1562
2046
  const snap = data;
1563
2047
  return h("div", null, snap.token?.present === false ? h("div", {
1564
2048
  style: S.formNote,
@@ -1608,9 +2092,23 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1608
2092
  error: providerError,
1609
2093
  onToggle: toggleProvider,
1610
2094
  onSaveList: saveRoster,
2095
+ onSaveContextWindow: saveContextWindow,
1611
2096
  onReset: resetProvider,
1612
2097
  tokenPresent: data?.token?.present === true,
1613
2098
  tt
2099
+ })), h(SectionCard, {
2100
+ title: tt("section.draw"),
2101
+ open: openSections.draw,
2102
+ onToggle: () => toggleSection("draw"),
2103
+ tt
2104
+ }, h(DrawCard, {
2105
+ draw,
2106
+ busy: drawBusy,
2107
+ error: drawError,
2108
+ onToggle: toggleDraw,
2109
+ onReset: resetDraw,
2110
+ tokenPresent: data?.token?.present === true,
2111
+ tt
1614
2112
  })));
1615
2113
  const accessBody = () => h("div", null, h(SectionCard, {
1616
2114
  title: tt("section.token"),
@@ -1690,6 +2188,175 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1690
2188
  init_styles();
1691
2189
  }));
1692
2190
 
2191
+ //#endregion
2192
+ //#region src/client/composer-usage-pill.ts
2193
+ /**
2194
+ * 一个候选自带的 provider,空串算「没有」。
2195
+ *
2196
+ * `typeof provider === "string"` 是不够的判据:`""` 也是字符串。照类型判定会让
2197
+ * `{ provider: "" }` 被当成一个「可用的 next」,于是它遮住仍然可用的 `lastUsed`
2198
+ * ——切模型那一帧胶囊就消失。所以这里判的是**非空字符串**。
2199
+ */
2200
+ function providerOf(candidate) {
2201
+ const provider = candidate?.provider;
2202
+ return typeof provider === "string" && provider !== "" ? provider : void 0;
2203
+ }
2204
+ /**
2205
+ * 会话当前指向的 provider。
2206
+ *
2207
+ * `next` 优先于 `lastUsed`(切换已落地但还没发出去时,选择器显示的就是 `next`),
2208
+ * 但**只在 `next` 真的带了一个非空 provider 时**才优先:picker 刚切完、新选择还没
2209
+ * 落到 `next` 上的那几帧 `next` 是 `{}` 或 `{ provider: "" }`,照「有 next 就用
2210
+ * next」读会得到 `undefined`,于是归属模型在「切换」这个动作本身上把胶囊藏起来。
2211
+ * 两侧都没有可用 provider 时返回 `undefined`——那才是真的判不出来。
2212
+ */
2213
+ function selectedProviderOf(projection) {
2214
+ const source = projection ?? {};
2215
+ return providerOf(source.next) ?? providerOf(source.lastUsed);
2216
+ }
2217
+ /**
2218
+ * 余额读数;`null` 表示**不知道**(没读到 / 上游没给 / 字段不是有限数)。
2219
+ *
2220
+ * 特意不接受 `error` 参数做二次判断:错误文本只影响 tooltip 的措辞,不影响
2221
+ * 「数字可不可信」。把两者揉在一个判定里,会让「上次读到的 143 仍然有效」这种
2222
+ * 情形(`error` 非空但 `available` 有值)被误打成未知。
2223
+ */
2224
+ function availableOf(balance) {
2225
+ const available = balance?.available;
2226
+ return typeof available === "number" && Number.isFinite(available) ? available : null;
2227
+ }
2228
+ /** 今日本地调用次数;`null` 同上,表示读不到(不是 0)。 */
2229
+ function usedTodayOf(snapshot) {
2230
+ const used = snapshot?.quota?.daily?.usedLocal;
2231
+ return typeof used === "number" && Number.isFinite(used) ? used : null;
2232
+ }
2233
+ /** 由余额读数定档;`null`(未读到)与 `0`(读到了、就是零)在此分道。 */
2234
+ function tierOf(available) {
2235
+ if (available === null) return "unknown";
2236
+ return available > 0 ? "ok" : "exhausted";
2237
+ }
2238
+ /** 「余额」段的正文:读不到画「—」+ 单位,读到了画数字 + 单位。 */
2239
+ function balanceText(available, tt) {
2240
+ return `${available === null ? "—" : count(available)} ${tt("composer.balanceUnit")}`;
2241
+ }
2242
+ /** 「今日本地」段的正文:读不到画「—」,读到了画次数。 */
2243
+ function callsText(usedToday, tt) {
2244
+ return format(tt("composer.localCalls"), { calls: usedToday === null ? "—" : count(usedToday) });
2245
+ }
2246
+ /**
2247
+ * 胶囊正文:余额段 + 本地次数段,各带各的口径。两段都必须出现——只画有值的那
2248
+ * 一段会让「哪一段是空的」变成排版事故(读者分不清是没读到还是这一栏根本不在),
2249
+ * 而「—」明确说了是前者。
2250
+ */
2251
+ function readingText(available, usedToday, tt) {
2252
+ return `${balanceText(available, tt)} · ${callsText(usedToday, tt)}`;
2253
+ }
2254
+ /**
2255
+ * 数字是否已经算「上次数据」。
2256
+ *
2257
+ * 两个触发条件各自独立:读失败(`error` 非空,上一轮的数值还留在屏幕上)本身就是
2258
+ * 陈旧;而长时间没有**成功**的读(`updatedAt` 不动)说明定时轮询没在按时回来
2259
+ * ——失败退避或页面被挂起都会这样。取 3 个声明周期而不是 1 个:一次偶发失败就会
2260
+ * 拉长间隔,卡在 1 个周期上会让每一次抖动都把读数打成陈旧的。
2261
+ *
2262
+ * `updatedAt === 0` 表示**从没读到过**,那不是「陈旧」而是「还没有」——调用方据此
2263
+ * 决定不标;本函数只在有读数的前提下判断,故返回 false。
2264
+ */
2265
+ function staleOf(input) {
2266
+ if (input.error !== null && input.error !== void 0) return input.updatedAt > 0;
2267
+ return input.updatedAt > 0 && input.now - input.updatedAt > input.cadenceMs * STALE_CADENCES;
2268
+ }
2269
+ /**
2270
+ * tooltip 的一行:只把**读者下一步会问的**事实拼进来——数字是哪来的(前缀)、
2271
+ * 余额为什么没有(错误原文)、什么时候读的(fetchedAt)、本地计数为什么没有
2272
+ * (quota 那一路的提示)、以及数字是不是旧的。逐段 `filter` 掉空值,不留下
2273
+ * 「 · · 」这样的空槽。
2274
+ */
2275
+ function titleOf(props) {
2276
+ const { tt, balance, usedToday, stale } = props;
2277
+ const parts = [tt("composer.pillTitle")];
2278
+ const failure = balance?.error;
2279
+ parts.push(typeof failure === "string" && failure !== "" ? format(tt("balance.unavailable"), { error: failure }) : null);
2280
+ const fetchedAt = balance?.fetchedAt;
2281
+ parts.push(typeof fetchedAt === "string" && fetchedAt !== "" ? format(tt("balance.fetched"), { time: isoTime(fetchedAt) }) : null);
2282
+ parts.push(usedToday === null ? tt("quota.unavailable") : null);
2283
+ parts.push(stale ? tt("composer.stale") : null);
2284
+ return parts.filter((part) => part !== null).join(" · ");
2285
+ }
2286
+ /**
2287
+ * 内层:只在会话选中本插件的模型时挂载。轮询 hook 在这里,被挂/摘即起停——
2288
+ * 非归属 provider 的会话不发任何请求。
2289
+ */
2290
+ function ModelscopePill(props) {
2291
+ const { tt } = props;
2292
+ const { data, error, loadedOnce, updatedAt } = useSnapshotPolling();
2293
+ const balance = data?.balance ?? null;
2294
+ const available = availableOf(balance);
2295
+ const usedToday = usedTodayOf(data);
2296
+ const cadenceMs = statedCadenceMs(data?.pollSeconds, DEFAULT_CADENCE_MS);
2297
+ const stale = staleOf({
2298
+ error,
2299
+ updatedAt,
2300
+ now: Date.now(),
2301
+ cadenceMs
2302
+ });
2303
+ const pending = !loadedOnce && data === null;
2304
+ const tier = tierOf(available);
2305
+ const title = titleOf({
2306
+ tt,
2307
+ balance,
2308
+ usedToday,
2309
+ stale
2310
+ });
2311
+ return h("button", {
2312
+ type: "button",
2313
+ className: "dsh-modelscope-composer-pill",
2314
+ title,
2315
+ "aria-label": title,
2316
+ style: S.pillTrigger
2317
+ }, h("span", {
2318
+ className: "dsh-modelscope-composer-pill-dot",
2319
+ "aria-hidden": "true",
2320
+ style: {
2321
+ ...S.pillDot,
2322
+ background: TIER_INK[tier]
2323
+ }
2324
+ }), h("span", {
2325
+ className: "dsh-modelscope-composer-pill-plan",
2326
+ style: S.pillPlan
2327
+ }, `${tt("composer.prefix")} · `), h("span", {
2328
+ className: "dsh-modelscope-composer-pill-text",
2329
+ style: tier === "exhausted" ? S.pillTierExhaustedText : void 0
2330
+ }, pending ? "…" : readingText(available, usedToday, tt)), stale ? h("span", {
2331
+ className: "dsh-modelscope-composer-pill-stale",
2332
+ style: S.pillStale
2333
+ }, ` · ${tt("composer.stale")}`) : null);
2334
+ }
2335
+ /**
2336
+ * 外层门控:固定一处调 `useProjection`,再决定内层挂谁。非归属 provider 返回
2337
+ * `null`(不渲染任何东西,也不跑任何请求)。
2338
+ */
2339
+ function ComposerUsagePill(props) {
2340
+ const selection = props.useProjection?.("modelSelection");
2341
+ if (selectedProviderOf(selection) !== "modelscope-token-plan") return null;
2342
+ return h(ModelscopePill, { tt: props.tt });
2343
+ }
2344
+ var MODELSCOPE_PROVIDER, DEFAULT_CADENCE_MS, STALE_CADENCES, TIER_INK;
2345
+ var init_composer_usage_pill = __esmMin((() => {
2346
+ init_runtime();
2347
+ init_styles();
2348
+ init_format();
2349
+ init_use_snapshot_polling();
2350
+ MODELSCOPE_PROVIDER = "modelscope-token-plan";
2351
+ DEFAULT_CADENCE_MS = 3e4;
2352
+ STALE_CADENCES = 3;
2353
+ TIER_INK = {
2354
+ ok: S.pillTierOk,
2355
+ exhausted: S.pillTierExhausted,
2356
+ unknown: S.pillTierUnknown
2357
+ };
2358
+ }));
2359
+
1693
2360
  //#endregion
1694
2361
  //#region src/client/apply.ts
1695
2362
  /**
@@ -1732,6 +2399,17 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1732
2399
  } catch (error) {
1733
2400
  console.warn(`[${NS}] config card registration failed:`, error);
1734
2401
  }
2402
+ try {
2403
+ disposers.push(ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
2404
+ name: "conversation.input.right",
2405
+ id: `${NS}-composer-pill`,
2406
+ locale: NS,
2407
+ order: 4,
2408
+ inject: () => ({ tt })
2409
+ }, ComposerUsagePill)));
2410
+ } catch (error) {
2411
+ console.warn(`[${NS}] composer pill registration failed:`, error);
2412
+ }
1735
2413
  ctx.effect(() => () => {
1736
2414
  for (const dispose of disposers.splice(0)) try {
1737
2415
  dispose();
@@ -1743,6 +2421,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1743
2421
  init_const();
1744
2422
  init_i18n();
1745
2423
  init_panel_page();
2424
+ init_composer_usage_pill();
1746
2425
  inject = ["slots", "locale"];
1747
2426
  }));
1748
2427
 
@@ -1758,6 +2437,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1758
2437
  init_styles();
1759
2438
  init_cards();
1760
2439
  init_panel_page();
2440
+ init_composer_usage_pill();
1761
2441
  init_use_polling_interval();
1762
2442
  init_use_snapshot_polling();
1763
2443
  init_http();
@@ -1767,6 +2447,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1767
2447
  const panel = Object.freeze({
1768
2448
  interpretSnapshot,
1769
2449
  viewOf,
2450
+ providerOf: providerOf$1,
1770
2451
  errorOfStatus,
1771
2452
  dictionaries: Object.freeze({
1772
2453
  zh,
@@ -1787,12 +2468,24 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1787
2468
  usePollingInterval,
1788
2469
  useSnapshotPolling,
1789
2470
  getJson,
1790
- postJson
2471
+ postJson,
2472
+ writeFailure,
2473
+ MODELSCOPE_PROVIDER,
2474
+ selectedProviderOf,
2475
+ availableOf,
2476
+ usedTodayOf,
2477
+ tierOf,
2478
+ balanceText,
2479
+ callsText,
2480
+ readingText,
2481
+ staleOf
1791
2482
  }),
1792
2483
  components: Object.freeze({
1793
2484
  BalanceCard,
2485
+ ComposerUsagePill,
1794
2486
  EventsList,
1795
2487
  LocalDailyCard,
2488
+ ModelscopePill,
1796
2489
  ModelUsageTable,
1797
2490
  PanelPage,
1798
2491
  ProviderCard,