dsh-connect-modelscope-token-plan 0.2.0 → 0.4.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
@@ -11,18 +11,61 @@ var dsh_connect_modelscope_token_plan_client = (function() {
11
11
  };
12
12
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
13
13
 
14
+ //#endregion
15
+ //#region src/shared/wire.ts
16
+ var HIDE_ALL_MODELS, SNAPSHOT_REQUIRED_KEYS;
17
+ var init_wire = __esmMin((() => {
18
+ HIDE_ALL_MODELS = "__hide_all__";
19
+ SNAPSHOT_REQUIRED_KEYS = Object.freeze([
20
+ "ok",
21
+ "name",
22
+ "version",
23
+ "now",
24
+ "pollSeconds",
25
+ "cacheSeconds",
26
+ "token",
27
+ "balance",
28
+ "quota",
29
+ "events",
30
+ "trend",
31
+ "models",
32
+ "provider",
33
+ "shapeWarnings",
34
+ "quotaError"
35
+ ]);
36
+ }));
37
+
14
38
  //#endregion
15
39
  //#region src/client/const.ts
16
40
  function modelscopeModelUrl(id) {
17
- return `${MODELSCOPE_MODEL_URL_BASE}/${encodeURI(id)}`;
41
+ return `${MODELSCOPE_MODEL_URL_BASE}/${id.split("/").map(encodeURIComponent).join("/")}`;
18
42
  }
19
43
  /** 从 `owner/model` 取 owner(无斜杠时整段作 owner)。 */
20
44
  function catalogOwner(id) {
21
45
  const slash = id.indexOf("/");
22
46
  return slash >= 0 ? id.slice(0, slash) : id;
23
47
  }
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;
48
+ /**
49
+ * 徽章用的窗口短标注(tokens → "128K" / "1M")。
50
+ *
51
+ * K=1024、M=1024²,与档位值(131072…1048576)同一把尺。**不美化**:目录声明的
52
+ * 128000 显示成 "125K" 而不是 "128K"——标签必须能从数值反推出来,四舍五入到
53
+ * 好看的整数是撒谎。≥1M 用 M(`1.25M` 保留至多一位小数,去掉尾随 .0),
54
+ * ≥1024 用 K,更小的原样显示。
55
+ * @param {number} tokens - 窗口大小(tokens)。
56
+ * @returns {string} 短标注。
57
+ */
58
+ function formatWindow(tokens) {
59
+ if (tokens >= 1048576) {
60
+ const m = tokens / 1048576;
61
+ return `${m >= 10 || Number.isInteger(m) ? Math.round(m) : Math.round(m * 10) / 10}M`;
62
+ }
63
+ if (tokens >= 1024) return `${Math.round(tokens / 1024)}K`;
64
+ return String(tokens);
65
+ }
66
+ 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, MODELSCOPE_TOKEN_URL, MODELSCOPE_USAGE_URL, MODELSCOPE_MODEL_URL_BASE, FEATURED_OWNERS;
25
67
  var init_const = __esmMin((() => {
68
+ init_wire();
26
69
  NS = "dsh-connect-modelscope-token-plan";
27
70
  PANEL_ID = NS;
28
71
  SNAPSHOT_PATH = `/api/${NS}/snapshot`;
@@ -32,8 +75,10 @@ var dsh_connect_modelscope_token_plan_client = (function() {
32
75
  PROBE_PATH = `/api/${NS}/probe`;
33
76
  PROVIDER_PATH = `/api/${NS}/provider`;
34
77
  PROVIDER_ROSTER_PATH = `/api/${NS}/provider/roster`;
78
+ PROVIDER_CONTEXT_PATH = `/api/${NS}/provider/context`;
35
79
  PROVIDER_RESET_PATH = `/api/${NS}/provider/reset`;
36
- HIDE_ALL_MODELS = "__hide_all__";
80
+ DRAW_PATH = `/api/${NS}/draw`;
81
+ DRAW_RESET_PATH = `/api/${NS}/draw/reset`;
37
82
  MODELSCOPE_TOKEN_URL = "https://modelscope.cn/my/myaccesstoken";
38
83
  MODELSCOPE_USAGE_URL = "https://modelscope.cn/magicube/usage?tab=consume";
39
84
  MODELSCOPE_MODEL_URL_BASE = "https://www.modelscope.cn/models";
@@ -62,6 +107,10 @@ var dsh_connect_modelscope_token_plan_client = (function() {
62
107
  "panel.authError": "令牌被拒绝(401/403)。请到「接入」tab 更换访问令牌。",
63
108
  "panel.noToken": "还没有配置魔搭访问令牌。到「接入」tab 粘贴一枚 ms-… 令牌即可。",
64
109
  "panel.shapeDrift": "上游返回的结构可能有变:{detail}",
110
+ "panel.payloadError": "Host 返回了无法读取的响应(可能页面被登录墙或代理截获)。",
111
+ "shape.missingKeys": "Host 快照缺少必需的顶层键:{keys}",
112
+ "common.httpError": "无法读取 Host 响应(非预期响应,可能页面被登录墙截获)。",
113
+ "common.timeout": "请求超时,请重试。",
65
114
  "tab.quota": "额度",
66
115
  "tab.models": "模型",
67
116
  "tab.access": "接入",
@@ -81,8 +130,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
81
130
  "balance.unavailable": "魔粒余额暂不可读:{error}",
82
131
  "quota.headline": "今日 {calls} 次 · {models} 个模型",
83
132
  "quota.note": "本地口径:只统计经本插件的调用,直连魔搭的其它客户端不计入;消耗总量以官方魔粒余额为准。",
84
- "models.none": "目录暂不可读:{error}",
85
- "models.loading": "读取目录中…",
133
+ "quota.unavailable": "本地计数本次读不到(原因见上方提示)",
86
134
  "probe.validity": "验令牌(零额度)",
87
135
  "probe.validOk": "令牌有效(HTTP {status})",
88
136
  "probe.fail": "失败:{error}",
@@ -94,6 +142,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
94
142
  "provider.enable": "把魔搭模型接入 DSH 模型选择器",
95
143
  "provider.on": "已接入",
96
144
  "provider.off": "未接入",
145
+ "provider.busy": "切换中…",
97
146
  "provider.registered": "已注册(模型可在选择器里选用)",
98
147
  "provider.notRegistered": "未注册",
99
148
  "provider.llmMissing": "本机 Host 未提供 LLM 注册服务",
@@ -110,18 +159,45 @@ var dsh_connect_modelscope_token_plan_client = (function() {
110
159
  "provider.notConfigured": "还没有访问令牌,先到「接入」tab 配置",
111
160
  "provider.source.panel": "面板",
112
161
  "provider.source.config": "配置",
162
+ "provider.contextWindow": "上下文窗口",
163
+ "provider.contextDefault": "默认(跟随目录)",
164
+ "provider.contextHint": "目录没声明窗口的模型按此值生效;保存后立即重新注册",
165
+ "provider.contextCuratedTitle": "窗口来自内置策展表(models.dev 交叉真值),平台未亲口声明;带 * 标记",
166
+ "section.draw": "出图工具",
167
+ "draw.enable": "给 agent 注册 modelscope_draw_image 工具",
168
+ "draw.on": "已开启",
169
+ "draw.off": "未开启",
170
+ "draw.busy": "切换中…",
171
+ "draw.mounted": "已注册:{tool}",
172
+ "draw.notMounted": "未注册——勾选上方开关即可开启。",
173
+ "draw.restartHint": "开关立即保存,但工具的挂载发生在**下次 Host 启动**(工具注册无法撤销)。",
174
+ "draw.models": "出图将使用以下模型",
175
+ "draw.badge": "出图",
176
+ "draw.noCandidates": "当前目录里没有出图模型(平台声明 image-to-image / text-to-image 的条目为空);出图不可用。",
177
+ "draw.needsKey": "还没有访问令牌,先到「接入」tab 配置。",
178
+ "draw.reset": "回到默认",
179
+ "draw.error": "出图设置保存失败:{error}",
180
+ "draw.source.panel": "面板",
181
+ "draw.source.config": "配置",
182
+ "draw.absent.noTools": "开关已开,但本机 Host 未提供 agent tools 服务,出图工具缺席(Host 侧限制,非配置问题)。",
183
+ "draw.absent.peerLoad": "开关已开,但 tools 模块加载失败,出图工具缺席(Host 问题,非配置)。",
184
+ "draw.absent.registry": "开关已开,但工具注册被 Host 拒绝,出图工具缺席(Host 问题,非配置)。",
185
+ "draw.absent.unknown": "开关已开,但出图工具未挂载,原因未知。",
113
186
  "trend.none": "还没有本地调用记录。",
114
187
  "trend.legend": "柱长相对区间内最大值,仅本地口径。",
115
188
  "events.none": "暂无事件。",
116
189
  "events.quota": "额度",
117
190
  "events.rate_limit": "限频",
118
191
  "events.error": "错误",
192
+ "events.unknown": "未知",
119
193
  "token.status": "状态:{present}(来源 {source},校验 {valid})",
120
194
  "token.save": "保存",
121
195
  "token.forget": "忘掉已保存",
122
196
  "token.placeholder": "粘贴 ms-… 访问令牌",
197
+ "token.ariaLabel": "魔搭访问令牌",
123
198
  "token.hint": "在魔搭「访问令牌」页生成。保存即生效(写入 DSH 凭据服务,不依赖重启)。环境变量 MODELSCOPE_API_KEY 只在 DSH 启动时读一次,且优先级高于此处保存的值——设了它就别再用面板存。",
124
199
  "token.ephemeral": "此 Host 没有凭据服务:面板保存的令牌重启即丢,请改用凭据服务或环境变量。",
200
+ "token.readError": "读不到凭据存储,无法确认令牌状态(不代表没配令牌):{error}",
125
201
  "token.link": "打开魔搭访问令牌页 →",
126
202
  "source.credentials": "凭据服务",
127
203
  "source.env": "环境变量",
@@ -130,7 +206,13 @@ var dsh_connect_modelscope_token_plan_client = (function() {
130
206
  "present.yes": "已配置",
131
207
  "present.no": "未配置",
132
208
  "validity.yes": "有效",
133
- "validity.no": "未校验"
209
+ "validity.no": "未校验",
210
+ "validity.bad": "无效",
211
+ "composer.prefix": "魔搭",
212
+ "composer.pillTitle": "魔搭额度:官方魔粒余额 + 今日本地调用次数",
213
+ "composer.balanceUnit": "魔粒",
214
+ "composer.localCalls": "今日本地 {calls} 次",
215
+ "composer.stale": "上次数据"
134
216
  };
135
217
  en = {
136
218
  "panel.title": "ModelScope Access · Quota",
@@ -146,6 +228,10 @@ var dsh_connect_modelscope_token_plan_client = (function() {
146
228
  "panel.authError": "Token rejected (401/403). Replace the access token in the Access tab.",
147
229
  "panel.noToken": "No ModelScope access token configured yet. Paste an ms-… token in the Access tab.",
148
230
  "panel.shapeDrift": "Upstream shape may have changed: {detail}",
231
+ "panel.payloadError": "Host returned an unreadable response (a login wall or proxy may have intercepted it).",
232
+ "shape.missingKeys": "Host snapshot omitted required top-level key(s): {keys}",
233
+ "common.httpError": "Unexpected Host response (a login wall or proxy may have intercepted it).",
234
+ "common.timeout": "Request timed out; please retry.",
149
235
  "tab.quota": "Quota",
150
236
  "tab.models": "Models",
151
237
  "tab.access": "Access",
@@ -165,8 +251,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
165
251
  "balance.unavailable": "Magicube balance temporarily unreadable: {error}",
166
252
  "quota.headline": "Today {calls} calls · {models} models",
167
253
  "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…",
254
+ "quota.unavailable": "Local counters could not be read this time (reason in the notices above)",
170
255
  "probe.validity": "Verify token (free)",
171
256
  "probe.validOk": "Token valid (HTTP {status})",
172
257
  "probe.fail": "Failed: {error}",
@@ -178,6 +263,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
178
263
  "provider.enable": "Register ModelScope models in the DSH model picker",
179
264
  "provider.on": "On",
180
265
  "provider.off": "Off",
266
+ "provider.busy": "Switching…",
181
267
  "provider.registered": "Registered (models selectable in the picker)",
182
268
  "provider.notRegistered": "Not registered",
183
269
  "provider.llmMissing": "This Host exposes no LLM service",
@@ -194,18 +280,45 @@ var dsh_connect_modelscope_token_plan_client = (function() {
194
280
  "provider.notConfigured": "No token yet — configure it in the Access tab",
195
281
  "provider.source.panel": "panel",
196
282
  "provider.source.config": "config",
283
+ "provider.contextWindow": "Context window",
284
+ "provider.contextDefault": "Default (catalog)",
285
+ "provider.contextHint": "Applies to models whose catalog entry declares no window; re-registers immediately",
286
+ "provider.contextCuratedTitle": "Window from the built-in curated table (models.dev cross-check), not declared by the platform; marked with *",
287
+ "section.draw": "Draw tool",
288
+ "draw.enable": "Register the modelscope_draw_image tool for the agent",
289
+ "draw.on": "On",
290
+ "draw.off": "Off",
291
+ "draw.busy": "Switching…",
292
+ "draw.mounted": "Registered: {tool}",
293
+ "draw.notMounted": "Not registered — tick the switch above.",
294
+ "draw.restartHint": "The switch saves at once, but the tool mounts at the NEXT Host start (a tool registration cannot be undone).",
295
+ "draw.models": "Draw calls will use the models below",
296
+ "draw.badge": "image",
297
+ "draw.noCandidates": "This catalogue declares no image model (no entry with image-to-image / text-to-image); drawing is unavailable.",
298
+ "draw.needsKey": "No token yet — configure it in the Access tab.",
299
+ "draw.reset": "Reset",
300
+ "draw.error": "Saving the draw settings failed: {error}",
301
+ "draw.source.panel": "panel",
302
+ "draw.source.config": "config",
303
+ "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).",
304
+ "draw.absent.peerLoad": "The switch is on, but the tools module failed to load; the draw tool is absent (a Host fault, not config).",
305
+ "draw.absent.registry": "The switch is on, but the Host refused the tool registration; the draw tool is absent (a Host fault, not config).",
306
+ "draw.absent.unknown": "The switch is on, but the draw tool is not mounted; the reason is unknown.",
197
307
  "trend.none": "No local call records yet.",
198
308
  "trend.legend": "Bar lengths are relative to the maximum in the range (local scope only).",
199
309
  "events.none": "No events.",
200
310
  "events.quota": "Quota",
201
311
  "events.rate_limit": "Rate limit",
202
312
  "events.error": "Error",
313
+ "events.unknown": "Unknown",
203
314
  "token.status": "Status: {present} (source {source}, check {valid})",
204
315
  "token.save": "Save",
205
316
  "token.forget": "Forget saved",
206
317
  "token.placeholder": "Paste an ms-… access token",
318
+ "token.ariaLabel": "ModelScope access token",
207
319
  "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
320
  "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.",
321
+ "token.readError": "Could not read the credential store, so the token state is unknown (this does NOT mean no token is configured): {error}",
209
322
  "token.link": "Open the ModelScope access-token page →",
210
323
  "source.credentials": "credentials",
211
324
  "source.env": "environment",
@@ -214,13 +327,23 @@ var dsh_connect_modelscope_token_plan_client = (function() {
214
327
  "present.yes": "configured",
215
328
  "present.no": "not configured",
216
329
  "validity.yes": "verified",
217
- "validity.no": "unverified"
330
+ "validity.no": "unverified",
331
+ "validity.bad": "invalid",
332
+ "composer.prefix": "ModelScope",
333
+ "composer.pillTitle": "ModelScope quota: official Magicube balance + today's local call count",
334
+ "composer.balanceUnit": "Magicubes",
335
+ "composer.localCalls": "local today {calls}",
336
+ "composer.stale": "stale data"
218
337
  };
219
338
  }));
220
339
 
221
340
  //#endregion
222
341
  //#region src/client/format.ts
223
- /** Time and number formatters(与姊妹插件 format.ts 受控复制,按需裁剪)。 */
342
+ /**
343
+ * Time and number formatters(与 sensenova 的 format.ts 同源、按需裁剪——去注释后
344
+ * 代码行重叠 73%,与 agnes 那份已分家(45%)。2026-10-16 实测,复核
345
+ * `node tools/dev/cross-repo-drift.mjs`)。
346
+ */
224
347
  /**
225
348
  * Host 声明的 cadence(秒)转毫秒;无可用数字时回退。Host 在源头已 clamp,
226
349
  * 面板不做第二次 opinion;亚秒值向上取整到 1s(setInterval 的 0 = 尽快)。
@@ -248,7 +371,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
248
371
  if (number >= 1e4) return Math.round(number).toLocaleString();
249
372
  return String(Math.round(number * 100) / 100);
250
373
  }
251
- /** 填 `{token}` 模板。 */
374
+ /** 填 `{key}` 模板:vars 的每个键对应模板里的同名占位符。 */
252
375
  function format(template, vars) {
253
376
  let text = template;
254
377
  for (const [key, value] of Object.entries(vars || {})) text = text.split(`{${key}}`).join(String(value));
@@ -350,7 +473,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
350
473
  fontSize: 13,
351
474
  color: "var(--dsw-alias-label-secondary)",
352
475
  cursor: "pointer",
353
- outline: "none"
476
+ outlineOffset: -2
354
477
  },
355
478
  tabActive: {
356
479
  color: "var(--dsw-alias-label-primary)",
@@ -632,38 +755,87 @@ var dsh_connect_modelscope_token_plan_client = (function() {
632
755
  borderRadius: 12,
633
756
  background: "var(--dsw-alias-bg-layer-1)",
634
757
  padding: "12px 14px"
635
- }
758
+ },
759
+ pillTrigger: {
760
+ appearance: "none",
761
+ WebkitAppearance: "none",
762
+ height: 28,
763
+ borderStyle: "none",
764
+ borderWidth: 0,
765
+ background: "none",
766
+ color: "inherit",
767
+ display: "inline-flex",
768
+ alignItems: "center",
769
+ gap: 6,
770
+ padding: "0 10px",
771
+ borderRadius: 999,
772
+ fontSize: 12,
773
+ fontWeight: 600,
774
+ lineHeight: "18px",
775
+ cursor: "default",
776
+ flexShrink: 0,
777
+ whiteSpace: "nowrap"
778
+ },
779
+ pillDot: {
780
+ width: 8,
781
+ height: 8,
782
+ borderRadius: 999,
783
+ flex: "none",
784
+ background: "var(--dsw-alias-label-secondary)"
785
+ },
786
+ pillPlan: { opacity: .72 },
787
+ pillStale: {
788
+ opacity: .62,
789
+ fontStyle: "italic"
790
+ },
791
+ pillTierOk: "#22c55e",
792
+ pillTierExhausted: "#ef4444",
793
+ pillTierUnknown: "#9ca3af",
794
+ pillTierExhaustedText: { color: "var(--dsw-alias-state-error-primary)" }
636
795
  };
637
796
  }));
638
797
 
639
798
  //#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
- ]);
799
+ //#region src/shared/codes.ts
800
+ var CODE;
801
+ var init_codes = __esmMin((() => {
802
+ CODE = Object.freeze({
803
+ CONFIG_ERROR: "config_error",
804
+ NETWORK_ERROR: "network_error",
805
+ TIMEOUT_ERROR: "timeout_error",
806
+ AUTH_ERROR: "auth_error",
807
+ RATE_LIMITED: "rate_limited",
808
+ QUOTA_EXCEEDED: "quota_exceeded",
809
+ UPSTREAM_ERROR: "upstream_error",
810
+ /**
811
+ * 插件自身抛错(聚合器、状态层、路由之外的意外)。
812
+ *
813
+ * 曾经只作为 `soft()` / `failureCode()` 的兜底字符串出现在快照里,没进这张
814
+ * 表——于是 Client 的 `GUIDANCE_BY_CODE` 也没法映射它(那份映射被测试钉住
815
+ * 「每个键必须在 CODE 里」),最需要人看的内部错误反而没有引导文案。
816
+ * 正式收进码表,两端从此同一份真源。
817
+ */
818
+ INTERNAL_ERROR: "internal_error",
819
+ /**
820
+ * Client 自产码:Host 响应无法解读成快照(非 JSON / 缺 ok 字段)时,
821
+ * 面板用它走引导文案,而不是把裸英文串("unexpected payload")上屏。
822
+ *
823
+ * 本表是 `GUIDANCE_BY_CODE` 的键集真源——新增 client 码必须同时进
824
+ * 这张表,两侧才同步。
825
+ */
826
+ PAYLOAD_ERROR: "payload_error"
827
+ });
660
828
  }));
661
829
 
662
830
  //#endregion
663
831
  //#region src/client/snapshot.ts
664
832
  /**
665
- * 缺失块的空形状,**逐字对齐 Host 的降级实现**(`snapshot-aggregate.ts` 的
666
- * `providerDegraded` 与各 `soft()` 失败分支、TokenStatus 的失败分支)。
833
+ * 缺失块的空形状,**逐字对齐 Host 的降级实现**(`snapshot-aggregate.ts` 各
834
+ * `soft()` 失败分支、TokenStatus 的失败分支)。
835
+ *
836
+ * `provider` 块现在是**省略**而不是由 Host 塞一个降级块(2026-10-17):Host 那份
837
+ * 降级形状「看起来就是一个答案」,面板据此认为块在、不再走 `GET /provider` 兜底,
838
+ * 于是只能拿编出来的值渲染。省略之后,兜底真的跑起来,降级形状只剩这里一份。
667
839
  *
668
840
  * 这里的每一条都是「Host 那一路失败时真的产出的形状」,不是随手捏的:面板渲染
669
841
  * 的每一个字段都必须有一个与 Host 同构的空值,否则补出来的空块自己就会炸——
@@ -676,7 +848,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
676
848
  source: "none",
677
849
  valid: null,
678
850
  checkedAt: null,
679
- ephemeral: true
851
+ ephemeral: true,
852
+ readError: null
680
853
  };
681
854
  case "balance": return {
682
855
  available: null,
@@ -686,7 +859,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
686
859
  error: null
687
860
  };
688
861
  case "quota": return {
689
- daily: { usedLocal: 0 },
862
+ daily: { usedLocal: null },
690
863
  perModel: [],
691
864
  countingNote: "local-counting"
692
865
  };
@@ -729,17 +902,23 @@ var dsh_connect_modelscope_token_plan_client = (function() {
729
902
  */
730
903
  function interpretSnapshot(body) {
731
904
  const payload = body;
732
- if (payload && payload.ok === false) return {
905
+ if (payload && payload.ok === false) {
906
+ const code = typeof payload.code === "string" && payload.code !== "" ? payload.code : CODE.PAYLOAD_ERROR;
907
+ return {
908
+ data: null,
909
+ error: {
910
+ message: typeof payload.error === "string" && payload.error !== "" ? payload.error : null,
911
+ code
912
+ }
913
+ };
914
+ }
915
+ if (!payload || payload.ok !== true) return {
733
916
  data: null,
734
917
  error: {
735
- message: payload.error || "unexpected payload",
736
- code: payload.code
918
+ message: null,
919
+ code: CODE.PAYLOAD_ERROR
737
920
  }
738
921
  };
739
- if (!payload || payload.ok !== true) return {
740
- data: null,
741
- error: "unexpected payload"
742
- };
743
922
  const raw = payload;
744
923
  const missingKeys = SNAPSHOT_REQUIRED_KEYS.filter((key) => raw[key] === void 0);
745
924
  return {
@@ -752,19 +931,23 @@ var dsh_connect_modelscope_token_plan_client = (function() {
752
931
  * 把 wire/路由返回的 provider 块归一成 `ProviderStatus`;不合法一律降级。
753
932
  * 形状由 wire.ts 钉死,本函数只做防御性归一(旧 Host 可能缺字段)。
754
933
  */
755
- function providerOf(raw) {
934
+ function providerOf$1(raw) {
756
935
  if (raw === null || typeof raw !== "object") return DEGRADED_PROVIDER;
757
936
  const p = raw;
758
937
  const ids = Array.isArray(p.enabledIds) ? p.enabledIds.map((id) => String(id ?? "")).filter((id) => id !== "") : [];
759
938
  const roster = Array.isArray(p.roster) ? p.roster.map((entry) => {
760
939
  const row = entry ?? {};
761
940
  const id = String(row.id ?? "");
941
+ const w = row.contextWindow;
942
+ const contextSource = row.contextSource === "override" || row.contextSource === "curated" ? row.contextSource : "catalog";
762
943
  return {
763
944
  id,
764
945
  name: typeof row.name === "string" && row.name !== "" ? row.name : id,
765
946
  vision: row.vision === true,
766
947
  available: row.available !== false,
767
- quotaExhausted: row.quotaExhausted === true
948
+ quotaExhausted: row.quotaExhausted === true,
949
+ contextSource,
950
+ ...typeof w === "number" && Number.isFinite(w) && w > 0 ? { contextWindow: w } : {}
768
951
  };
769
952
  }).filter((entry) => entry.id !== "") : [];
770
953
  return {
@@ -773,10 +956,11 @@ var dsh_connect_modelscope_token_plan_client = (function() {
773
956
  llmAvailable: p.llmAvailable === true,
774
957
  registered: p.registered === true,
775
958
  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,
959
+ modelCount: typeof p.modelCount === "number" && Number.isFinite(p.modelCount) ? p.modelCount : roster.length,
960
+ enabledCount: typeof p.enabledCount === "number" && Number.isFinite(p.enabledCount) ? p.enabledCount : 0,
778
961
  allowed: p.allowed === "none" ? "none" : p.allowed === "list" ? "list" : "all",
779
962
  enabledIds: ids,
963
+ contextWindow: typeof p.contextWindow === "number" && Number.isFinite(p.contextWindow) && p.contextWindow > 0 ? p.contextWindow : null,
780
964
  roster
781
965
  };
782
966
  }
@@ -789,6 +973,33 @@ var dsh_connect_modelscope_token_plan_client = (function() {
789
973
  return `HTTP ${status}`;
790
974
  }
791
975
  /**
976
+ * 把 wire/路由返回的出图块归一成 `DrawStatus`;不合法一律降级。
977
+ *
978
+ * 与 `providerOf` 同一套防御性归一(旧 Host 可能缺字段)。`absentReason` 只认三个
979
+ * 已知码——上游若新增第四个,归一到 `null` 而不是把它透传成渲染不出的值;面板的
980
+ * 「缺席」措辞有兜底键,但**状态本身**宁可显示「未知」也不假装认识它。
981
+ * @param {unknown} raw - 原始块。
982
+ * @returns {DrawStatus} 归一后的状态。
983
+ */
984
+ function drawOf(raw) {
985
+ if (raw === null || typeof raw !== "object") return DEGRADED_DRAW;
986
+ const d = raw;
987
+ const candidates = Array.isArray(d.candidateIds) ? d.candidateIds.map((id) => String(id ?? "")).filter((id) => id !== "") : [];
988
+ const reason = d.absentReason;
989
+ const absentReason = reason === "no-tools-service" || reason === "peer-load-failed" || reason === "registry-refused" ? reason : null;
990
+ return {
991
+ enabled: d.enabled === true,
992
+ source: d.source === "panel" ? "panel" : "config",
993
+ candidateIds: candidates,
994
+ candidateCount: typeof d.candidateCount === "number" && Number.isFinite(d.candidateCount) ? d.candidateCount : candidates.length,
995
+ preferredModel: typeof d.preferredModel === "string" && d.preferredModel !== "" ? d.preferredModel : null,
996
+ preferredSource: d.preferredSource === "panel" ? "panel" : "config",
997
+ absentReason,
998
+ mountPending: d.mountPending === true,
999
+ toolName: typeof d.toolName === "string" && d.toolName !== "" ? d.toolName : "modelscope_draw_image"
1000
+ };
1001
+ }
1002
+ /**
792
1003
  * 面板决策:这张快照意味着什么。纯函数,Node 套件驱动同一个函数。
793
1004
  *
794
1005
  * `missingKeys` 是 `interpretSnapshot` 查出的缺失顶层键(非空 = 双端契约漂移)。
@@ -805,7 +1016,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
805
1016
  const guidanceKey = failure === null ? null : GUIDANCE_BY_CODE[failure.code] ?? null;
806
1017
  const guidance = guidanceKey === null ? null : guidanceKey === "panel.configError" ? format(tt(guidanceKey), { error: failure?.message }) : tt(guidanceKey);
807
1018
  const shapeWarnings = Array.isArray(data?.shapeWarnings) ? [...data.shapeWarnings] : [];
808
- if (missingKeys.length > 0) shapeWarnings.push(`snapshot: Host omitted required key(s): ${missingKeys.join(", ")}`);
1019
+ if (missingKeys.length > 0) shapeWarnings.push(format(tt("shape.missingKeys"), { keys: missingKeys.join(", ") }));
809
1020
  return {
810
1021
  failure,
811
1022
  needsSetup,
@@ -814,10 +1025,11 @@ var dsh_connect_modelscope_token_plan_client = (function() {
814
1025
  shapeWarnings
815
1026
  };
816
1027
  }
817
- var DEGRADED_PROVIDER, GUIDANCE_BY_CODE, FORM_EXCLUDED_CODES;
1028
+ var DEGRADED_PROVIDER, DEGRADED_DRAW, GUIDANCE_BY_CODE, FORM_EXCLUDED_CODES;
818
1029
  var init_snapshot = __esmMin((() => {
819
1030
  init_format();
820
1031
  init_wire();
1032
+ init_codes();
821
1033
  DEGRADED_PROVIDER = Object.freeze({
822
1034
  enabled: false,
823
1035
  source: "config",
@@ -828,8 +1040,20 @@ var dsh_connect_modelscope_token_plan_client = (function() {
828
1040
  enabledCount: 0,
829
1041
  allowed: "all",
830
1042
  enabledIds: [],
1043
+ contextWindow: null,
831
1044
  roster: []
832
1045
  });
1046
+ DEGRADED_DRAW = Object.freeze({
1047
+ enabled: false,
1048
+ source: "config",
1049
+ candidateIds: [],
1050
+ candidateCount: 0,
1051
+ preferredModel: null,
1052
+ preferredSource: "config",
1053
+ absentReason: null,
1054
+ mountPending: false,
1055
+ toolName: "modelscope_draw_image"
1056
+ });
833
1057
  GUIDANCE_BY_CODE = Object.freeze({
834
1058
  auth_error: "panel.authError",
835
1059
  config_error: "panel.configError",
@@ -838,7 +1062,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
838
1062
  upstream_error: "panel.upstream",
839
1063
  rate_limited: "panel.upstream",
840
1064
  quota_exceeded: "panel.upstream",
841
- internal_error: "panel.internalError"
1065
+ internal_error: "panel.internalError",
1066
+ payload_error: "panel.payloadError"
842
1067
  });
843
1068
  FORM_EXCLUDED_CODES = Object.freeze(/* @__PURE__ */ new Set([
844
1069
  "config_error",
@@ -847,7 +1072,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
847
1072
  "upstream_error",
848
1073
  "rate_limited",
849
1074
  "quota_exceeded",
850
- "internal_error"
1075
+ "internal_error",
1076
+ "payload_error"
851
1077
  ]));
852
1078
  }));
853
1079
 
@@ -899,8 +1125,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
899
1125
  inset: 0,
900
1126
  borderRadius: 999,
901
1127
  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)",
1128
+ border: `1px solid ${on ? BRAND : "var(--dsw-alias-border-l2, #36373b)"}`,
1129
+ background: on ? BRAND : "var(--dsw-alias-bg-layer-2, #2a2b31)",
904
1130
  transition: "background .15s, border-color .15s"
905
1131
  }
906
1132
  }, h("span", { style: {
@@ -921,12 +1147,80 @@ var dsh_connect_modelscope_token_plan_client = (function() {
921
1147
  var TRACK_W, TRACK_H, THUMB, TRAVEL;
922
1148
  var init_toggle_switch = __esmMin((() => {
923
1149
  init_runtime();
1150
+ init_styles();
924
1151
  TRACK_W = 30;
925
1152
  TRACK_H = 17;
926
1153
  THUMB = 12;
927
1154
  TRAVEL = 13;
928
1155
  }));
929
1156
 
1157
+ //#endregion
1158
+ //#region src/shared/allow-list.ts
1159
+ /**
1160
+ * 把线编码的 `enabledIds` 解析成联合——**全插件只有这一处解释**。
1161
+ *
1162
+ * 判据(与旧 `resolveAllowedList` 的承诺一致,且现在 `filterByEnabled` 等
1163
+ * 消费者也真的按它做):
1164
+ * 1. 非数组或空数组 → `all`(新装没做过选择,仍提供全部);
1165
+ * 2. 含哨兵(哪怕还混着别的 id)→ `none`(哨兵是独占态,优先于任何真实 id);
1166
+ * 3. 其余非空清单 → `list`(严格白名单;条目滤掉非字符串与空串、
1167
+ * 去重保序——落盘前 `provider-store` 已归一遍,这里是解释不是归一)。
1168
+ *
1169
+ * 注意 `[""]` 这类「滤完什么都不剩」的脏清单归 `list` 且 `ids: []`:过滤
1170
+ * 结果为空,与旧行为一致(它匹配不到任何真实 id)。
1171
+ * @param {unknown} enabledIds - 线编码的允许清单。
1172
+ * @returns {AllowedList} 联合。
1173
+ */
1174
+ function parseAllowedList(enabledIds) {
1175
+ if (!Array.isArray(enabledIds) || enabledIds.length === 0) return { mode: "all" };
1176
+ if (enabledIds.includes("__hide_all__")) return { mode: "none" };
1177
+ const ids = [];
1178
+ const seen = /* @__PURE__ */ new Set();
1179
+ for (const entry of enabledIds) {
1180
+ const id = typeof entry === "string" ? entry : "";
1181
+ if (id === "" || seen.has(id)) continue;
1182
+ seen.add(id);
1183
+ ids.push(id);
1184
+ }
1185
+ return {
1186
+ mode: "list",
1187
+ ids
1188
+ };
1189
+ }
1190
+ /**
1191
+ * 面板草稿 → 落盘清单的归一:哨兵独占态。
1192
+ *
1193
+ * 草稿含哨兵(「全部隐藏」)时落盘值只保留哨兵——不能让它与真实 id 混排:
1194
+ * Host 把混排判 `none`,而面板会把混排里的真实 id 画成勾中,「已启用 N」
1195
+ * 与实际提供 0 个撒谎(踩过,见 cards.ts 的注释)。其余情况剥掉哨兵、
1196
+ * 滤掉空串后原样落盘。
1197
+ * @param {readonly unknown[]} draft - 面板勾选的草稿。
1198
+ * @returns {string[]} 落盘的 `enabledIds`。
1199
+ */
1200
+ function normalizeRosterSave(draft) {
1201
+ return rosterSaveOf(parseAllowedList(draft));
1202
+ }
1203
+ /**
1204
+ * 联合 → 落盘清单({@link parseAllowedList} 的逆运算):`none` 序列化成
1205
+ * 哨兵清单,`all` 序列化成空清单(「不过滤」的拼写),`list` 原样带出 id。
1206
+ * @param {AllowedList} list - 联合。
1207
+ * @returns {string[]} 线编码的 `enabledIds`。
1208
+ */
1209
+ function rosterSaveOf(list) {
1210
+ switch (list.mode) {
1211
+ case "all": return [];
1212
+ case "none": return [HIDE_ALL_MODELS];
1213
+ case "list": return [...list.ids];
1214
+ }
1215
+ }
1216
+ /** 「全部隐藏」的落盘清单({@link rosterSaveOf} 的 `none` 特例;每次新数组)。 */
1217
+ function hideAllRoster() {
1218
+ return rosterSaveOf({ mode: "none" });
1219
+ }
1220
+ var init_allow_list = __esmMin((() => {
1221
+ init_wire();
1222
+ }));
1223
+
930
1224
  //#endregion
931
1225
  //#region src/client/cards.ts
932
1226
  /** 可折叠 section 卡:全宽头部按钮 + 旋转 chevron;open/onToggle 由 props 进。 */
@@ -982,12 +1276,17 @@ var dsh_connect_modelscope_token_plan_client = (function() {
982
1276
  */
983
1277
  function LocalDailyCard({ snapshot, tt }) {
984
1278
  const quota = snapshot.quota;
985
- const daily = quota?.daily ?? { usedLocal: 0 };
1279
+ const daily = quota?.daily ?? { usedLocal: null };
986
1280
  const perModel = Array.isArray(quota?.perModel) ? quota.perModel : [];
1281
+ const unknownUsed = !(typeof daily.usedLocal === "number" && Number.isFinite(daily.usedLocal));
1282
+ const usedText = unknownUsed ? "—" : count(daily.usedLocal);
987
1283
  return h("div", { style: S.card }, h("div", { style: S.poolName }, format(tt("quota.headline"), {
988
- calls: count(daily.usedLocal),
1284
+ calls: usedText,
989
1285
  models: count(perModel.length)
990
- })), h(ModelUsageTable, {
1286
+ })), unknownUsed ? h("div", { style: {
1287
+ ...S.formNote,
1288
+ color: "var(--dsw-alias-state-warn-primary)"
1289
+ } }, tt("quota.unavailable")) : null, h(ModelUsageTable, {
991
1290
  rows: perModel,
992
1291
  tt
993
1292
  }), h("div", { style: S.trendLegend }, tt("quota.note")));
@@ -1019,21 +1318,25 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1019
1318
  /** 近 N 天本地调用趋势(天桶;缺桶 = 该日 0 次,真实零)。 */
1020
1319
  function TrendBars({ buckets, tt }) {
1021
1320
  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,
1321
+ const rows = buckets.map((bucket) => ({
1322
+ dateKey: bucket.dateKey,
1323
+ calls: Number.isFinite(bucket.calls) ? Math.max(0, bucket.calls) : 0
1324
+ }));
1325
+ const max = Math.max(0, ...rows.map((row) => row.calls));
1326
+ return h("div", null, rows.map((row) => h("div", {
1327
+ key: row.dateKey,
1025
1328
  style: S.trendRowHead
1026
- }, h("span", { style: S.trendModel }, bucket.dateKey), h("div", {
1329
+ }, h("span", { style: S.trendModel }, row.dateKey), h("div", {
1027
1330
  style: {
1028
1331
  ...S.trendBar,
1029
1332
  flex: "1",
1030
1333
  marginLeft: 10
1031
1334
  },
1032
1335
  "aria-hidden": "true"
1033
- }, max > 0 && bucket.calls > 0 ? h("div", { style: {
1336
+ }, max > 0 && row.calls > 0 ? h("div", { style: {
1034
1337
  ...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")));
1338
+ width: `${row.calls / max * 100}%`
1339
+ } }) : null), h("span", { style: S.quotaUsed }, String(row.calls)))), h("div", { style: S.trendLegend }, tt("trend.legend")));
1037
1340
  }
1038
1341
  /** 事件流(429/错误),Newest-first。 */
1039
1342
  function EventsList({ events, tt }) {
@@ -1041,7 +1344,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1041
1344
  return h("div", null, events.map((event, index) => h("div", {
1042
1345
  key: `${event.at}-${index}`,
1043
1346
  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", {
1347
+ }, h("span", { style: S.quotaUsed }, isoTime(event.at)), h("span", { style: S.modelTag }, tt(EVENT_KIND_KEY[event.kind] ?? "events.unknown")), h("span", {
1045
1348
  style: {
1046
1349
  ...S.trendModel,
1047
1350
  flex: "1"
@@ -1054,8 +1357,11 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1054
1357
  const [value, setValue] = useState("");
1055
1358
  const source = token?.source ?? "none";
1056
1359
  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"), {
1360
+ const validKey = token?.valid === true ? "validity.yes" : token?.valid === false ? "validity.bad" : "validity.no";
1361
+ return h("div", null, token?.readError ? h("div", { style: {
1362
+ ...S.formNote,
1363
+ color: "var(--dsw-alias-state-warn-primary)"
1364
+ } }, format(tt("token.readError"), { error: token.readError })) : h("div", { style: S.quotaUsed }, format(tt("token.status"), {
1059
1365
  present: token?.present ? tt("present.yes") : tt("present.no"),
1060
1366
  source: tt(sourceKey),
1061
1367
  valid: tt(validKey)
@@ -1063,6 +1369,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1063
1369
  style: S.input,
1064
1370
  type: "password",
1065
1371
  placeholder: tt("token.placeholder"),
1372
+ "aria-label": tt("token.ariaLabel"),
1066
1373
  value,
1067
1374
  onChange: (event) => setValue(String(event.target.value ?? ""))
1068
1375
  }), h("button", {
@@ -1109,25 +1416,34 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1109
1416
  *
1110
1417
  * 允许清单的语义(路由 §9 钉死):空清单 = 不过滤 = 全部提供,所以「全部」
1111
1418
  * 发 `[]`;「全部隐藏」发哨兵 `HIDE_ALL_MODELS`(「什么都不提供」)。哨兵
1112
- * 不是任何真实模型 id,所以同一套 `draft.includes(id)` 的勾选判断天然不会把
1113
- * 它画成勾中——不需要特判。
1419
+ * 是**独占态**:draft 含哨兵时整个清单就是「全部隐藏」——勾一个真实模型会
1420
+ * 退出哨兵态(剔除哨兵),Host 落盘值带哨兵时只保留哨兵。**不能**让哨兵与
1421
+ * 真实 id 混排:Host 的 resolveAllowedList 判「含哨兵即 none(全隐藏)」,而
1422
+ * 面板若把混排清单里的真实 id 画成勾中、计数为 N,UI 就与落盘撒谎了。
1423
+ * 曾踩过:点「全部隐藏」后复选框仍可勾,勾出的混排清单保存后「已启用 1/N」
1424
+ * 而实际提供 0 个。
1114
1425
  *
1115
1426
  * 额度耗尽的模型保留在清单里(灰显、勾选框禁用),这是故意的:roster 是目录
1116
1427
  * 事实,picker 那边由 Host 自己丢掉它们,面板不替它删。
1117
1428
  */
1118
- function ProviderCard({ provider, busy, error, onToggle, onSaveList, onReset, tokenPresent, tt }) {
1119
- const status = providerOf(provider);
1429
+ function ProviderCard({ provider, busy, error, onToggle, onSaveList, onSaveContextWindow, onReset, tokenPresent, tt }) {
1430
+ const status = providerOf$1(provider);
1120
1431
  const enabled = status.enabled === true;
1121
1432
  const hostIds = status.enabledIds;
1122
1433
  const hostKey = useMemo(() => JSON.stringify(hostIds), [hostIds]);
1123
1434
  const [draft, setDraft] = useState(() => hostIds.slice());
1435
+ const hideAllMode = parseAllowedList(draft).mode === "none";
1124
1436
  useEffect(() => {
1125
- setDraft(hostIds);
1437
+ setDraft(normalizeRosterSave(hostIds));
1126
1438
  }, [hostKey]);
1127
1439
  const roster = status.roster;
1128
1440
  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;
1441
+ const toggleOne = (id) => setDraft((current) => {
1442
+ const ids = parseAllowedList(current);
1443
+ const kept = ids.mode === "list" ? [...ids.ids] : [];
1444
+ return kept.includes(id) ? kept.filter((x) => x !== id) : [...kept, id];
1445
+ });
1446
+ const ticked = hideAllMode ? 0 : roster.filter((row) => draft.includes(row.id)).length;
1131
1447
  const degraded = status.error === "unavailable";
1132
1448
  let statusNode;
1133
1449
  if (!degraded && typeof status.error === "string" && status.error !== "") statusNode = h("div", {
@@ -1157,6 +1473,61 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1157
1473
  color: "var(--dsw-alias-state-warn-primary)"
1158
1474
  } }, tt("provider.notConfigured")) : null;
1159
1475
  const sourceKey = status.source === "panel" ? "provider.source.panel" : "provider.source.config";
1476
+ const CONTEXT_TIERS = [
1477
+ {
1478
+ value: 131072,
1479
+ label: "128K"
1480
+ },
1481
+ {
1482
+ value: 262144,
1483
+ label: "256K"
1484
+ },
1485
+ {
1486
+ value: 524288,
1487
+ label: "512K"
1488
+ },
1489
+ {
1490
+ value: 1048576,
1491
+ label: "1M"
1492
+ }
1493
+ ];
1494
+ const contextNode = onSaveContextWindow === void 0 ? null : h("div", { style: {
1495
+ display: "flex",
1496
+ gap: 8,
1497
+ alignItems: "center",
1498
+ flexWrap: "wrap",
1499
+ margin: "0 0 8px"
1500
+ } }, h("label", {
1501
+ style: {
1502
+ fontSize: 12,
1503
+ color: "var(--dsw-alias-label-secondary)"
1504
+ },
1505
+ htmlFor: "provider-context-window"
1506
+ }, tt("provider.contextWindow")), h("select", {
1507
+ id: "provider-context-window",
1508
+ style: {
1509
+ ...S.rosterBulk,
1510
+ width: "auto"
1511
+ },
1512
+ disabled: busy,
1513
+ value: status.contextWindow === null ? "" : String(status.contextWindow),
1514
+ onChange: (event) => {
1515
+ const value = String(event.target?.value ?? "");
1516
+ onSaveContextWindow(value === "" ? null : Number(value));
1517
+ }
1518
+ }, h("option", {
1519
+ value: "",
1520
+ key: "default"
1521
+ }, tt("provider.contextDefault")), CONTEXT_TIERS.map((tier) => h("option", {
1522
+ value: String(tier.value),
1523
+ key: String(tier.value)
1524
+ }, tier.label))), h("span", {
1525
+ style: {
1526
+ ...S.muted,
1527
+ fontSize: 11
1528
+ },
1529
+ title: tt("provider.contextHint")
1530
+ }, tt("provider.contextHint")));
1160
1531
  const switchRow = h("div", { style: {
1161
1532
  display: "flex",
1162
1533
  gap: 10,
@@ -1168,6 +1539,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1168
1539
  onChange: () => onToggle(!enabled),
1169
1540
  busy,
1170
1541
  label: tt(enabled ? "provider.on" : "provider.off"),
1542
+ busyLabel: tt("provider.busy"),
1171
1543
  title: tt("provider.enable")
1172
1544
  }), h("span", { style: {
1173
1545
  fontSize: 11,
@@ -1199,8 +1571,9 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1199
1571
  style: S.rosterBulk,
1200
1572
  disabled: busy,
1201
1573
  onClick: () => {
1202
- setDraft([HIDE_ALL_MODELS]);
1203
- onSaveList([HIDE_ALL_MODELS]);
1574
+ const none = hideAllRoster();
1575
+ setDraft(none);
1576
+ onSaveList(none);
1204
1577
  }
1205
1578
  }, tt("provider.hideAll")), h("button", {
1206
1579
  type: "button",
@@ -1228,7 +1601,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1228
1601
  cursor: busy || unusable ? "default" : "pointer"
1229
1602
  } }, h("input", {
1230
1603
  type: "checkbox",
1231
- checked: draft.includes(id),
1604
+ checked: !hideAllMode && draft.includes(id),
1232
1605
  disabled: busy || unusable,
1233
1606
  onChange: () => toggleOne(id),
1234
1607
  style: S.modelCheck,
@@ -1236,7 +1609,13 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1236
1609
  }), h("span", {
1237
1610
  style: S.modelName,
1238
1611
  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", {
1612
+ }, row.name)), row.vision === true ? h("span", { style: S.modelBadge }, tt("provider.vision")) : null, typeof row.contextWindow === "number" ? h("span", {
1613
+ style: row.contextSource === "curated" ? {
1614
+ ...S.modelBadge,
1615
+ color: "var(--dsw-alias-label-secondary)"
1616
+ } : S.modelBadge,
1617
+ title: row.contextSource === "curated" ? tt("provider.contextCuratedTitle") : tt("provider.contextWindow")
1618
+ }, row.contextSource === "curated" ? formatWindow(row.contextWindow) + " *" : formatWindow(row.contextWindow)) : null, FEATURED_OWNERS.includes(catalogOwner(row.id)) ? h("span", {
1240
1619
  style: S.modelBadgeFeatured,
1241
1620
  title: tt("models.featuredTitle")
1242
1621
  }, tt("provider.featured")) : null, row.quotaExhausted === true ? h("span", { style: {
@@ -1259,7 +1638,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1259
1638
  disabled: busy,
1260
1639
  onClick: onReset
1261
1640
  }, tt("provider.reset"))));
1262
- return h("div", null, switchRow, statusNode, tokenHint, h("div", { style: {
1641
+ return h("div", null, switchRow, statusNode, tokenHint, contextNode, h("div", { style: {
1263
1642
  ...S.sectionTitle,
1264
1643
  margin: "18px 0 8px"
1265
1644
  } }, tt("provider.models")), rosterBlock, error !== null ? h("div", {
@@ -1267,7 +1646,114 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1267
1646
  role: "alert"
1268
1647
  }, error) : null);
1269
1648
  }
1270
- var EVENT_KIND_KEY;
1649
+ /**
1650
+ * 出图工具卡:开关、候选模型、挂载状态。
1651
+ *
1652
+ * **与 ProviderCard 的关键差异,也是这张卡存在的主要理由**:provider 开关翻转会
1653
+ * 立即重注册,而出图工具的开关翻转**要等下次 Host 启动**——peer 的 tools registry
1654
+ * 没有 unregister。所以这张卡必须把这件事说出来(`draw.restartHint`),否则用户
1655
+ * 翻开关后看不到任何变化,会以为坏了。
1656
+ *
1657
+ * 缺席原因是**分开渲染**的:`no-tools-service` 是「这个 Host 没提供该服务」(正常),
1658
+ * 另两种是 Host 缺陷。三种都给出各自的一句话,因为「开关开着但工具不在」如果没有
1659
+ * 解释,用户没有任何办法自己诊断。
1660
+ * @param {object} props - 渲染参数。
1661
+ * @returns {unknown} 卡片节点。
1662
+ */
1663
+ function DrawCard({ draw, busy, error, onToggle, onReset, tokenPresent, tt }) {
1664
+ const status = drawOf(draw);
1665
+ const enabled = status.enabled === true;
1666
+ const candidates = status.candidateIds;
1667
+ const sourceKey = status.source === "panel" ? "draw.source.panel" : "draw.source.config";
1668
+ const switchRow = h("div", { style: {
1669
+ display: "flex",
1670
+ gap: 10,
1671
+ alignItems: "center",
1672
+ flexWrap: "wrap",
1673
+ margin: "0 0 8px"
1674
+ } }, h(ToggleSwitch, {
1675
+ checked: enabled,
1676
+ onChange: () => onToggle(!enabled),
1677
+ busy,
1678
+ label: tt(enabled ? "draw.on" : "draw.off"),
1679
+ busyLabel: tt("draw.busy"),
1680
+ title: tt("draw.enable")
1681
+ }), h("span", { style: {
1682
+ fontSize: 11,
1683
+ color: "var(--dsw-alias-label-secondary)"
1684
+ } }, tt(sourceKey)));
1685
+ let statusNode;
1686
+ if (status.absentReason !== null) {
1687
+ const key = DRAW_ABSENT_KEY[status.absentReason] ?? "draw.absent.unknown";
1688
+ const severe = status.absentReason !== "no-tools-service";
1689
+ statusNode = h("div", {
1690
+ style: severe ? {
1691
+ ...S.formNote,
1692
+ color: "var(--dsw-alias-state-warn-primary)"
1693
+ } : S.formNote,
1694
+ role: "status"
1695
+ }, tt(key));
1696
+ } else if (enabled) statusNode = h("div", {
1697
+ style: {
1698
+ fontSize: 12,
1699
+ color: "var(--dsw-alias-label-secondary)"
1700
+ },
1701
+ role: "status"
1702
+ }, format(tt("draw.mounted"), { tool: status.toolName }));
1703
+ else statusNode = h("div", {
1704
+ style: {
1705
+ ...S.muted,
1706
+ fontSize: 12
1707
+ },
1708
+ role: "status"
1709
+ }, tt("draw.notMounted"));
1710
+ const restartHint = enabled ? h("div", { style: {
1711
+ ...S.formNote,
1712
+ color: "var(--dsw-alias-state-warn-primary)"
1713
+ } }, tt("draw.restartHint")) : null;
1714
+ const tokenHint = enabled && tokenPresent === false ? h("div", { style: {
1715
+ ...S.formNote,
1716
+ color: "var(--dsw-alias-state-warn-primary)"
1717
+ } }, tt("draw.needsKey")) : null;
1718
+ const candidateBlock = candidates.length === 0 ? h("div", { style: {
1719
+ ...S.muted,
1720
+ fontSize: 12,
1721
+ marginTop: 6
1722
+ } }, tt("draw.noCandidates")) : h("div", { style: {
1723
+ display: "flex",
1724
+ flexDirection: "column",
1725
+ gap: 4
1726
+ } }, candidates.map((id) => h("div", {
1727
+ key: id,
1728
+ style: S.modelRowHead
1729
+ }, h("code", {
1730
+ style: S.modelName,
1731
+ title: id
1732
+ }, id), h("span", { style: S.modelBadge }, tt("draw.badge")), h("a", {
1733
+ style: {
1734
+ ...S.formNote,
1735
+ margin: 0,
1736
+ marginLeft: "auto"
1737
+ },
1738
+ href: modelscopeModelUrl(id),
1739
+ target: "_blank",
1740
+ rel: "noreferrer",
1741
+ title: tt("models.viewOnSiteTitle")
1742
+ }, tt("models.viewOnSite")))));
1743
+ return h("div", null, switchRow, statusNode, restartHint, tokenHint, h("div", { style: {
1744
+ ...S.sectionTitle,
1745
+ margin: "18px 0 8px"
1746
+ } }, tt("draw.models")), candidateBlock, h("div", { style: S.rosterFoot }, h("button", {
1747
+ type: "button",
1748
+ style: S.button,
1749
+ disabled: busy,
1750
+ onClick: onReset
1751
+ }, tt("draw.reset"))), error !== null ? h("div", {
1752
+ style: S.formError,
1753
+ role: "alert"
1754
+ }, error) : null);
1755
+ }
1756
+ var EVENT_KIND_KEY, DRAW_ABSENT_KEY;
1271
1757
  var init_cards = __esmMin((() => {
1272
1758
  init_format();
1273
1759
  init_runtime();
@@ -1275,18 +1761,44 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1275
1761
  init_const();
1276
1762
  init_snapshot();
1277
1763
  init_toggle_switch();
1764
+ init_allow_list();
1278
1765
  EVENT_KIND_KEY = {
1279
1766
  quota: "events.quota",
1280
1767
  rate_limit: "events.rate_limit",
1281
1768
  error: "events.error"
1282
1769
  };
1770
+ DRAW_ABSENT_KEY = {
1771
+ "no-tools-service": "draw.absent.noTools",
1772
+ "peer-load-failed": "draw.absent.peerLoad",
1773
+ "registry-refused": "draw.absent.registry"
1774
+ };
1283
1775
  }));
1284
1776
 
1285
1777
  //#endregion
1286
1778
  //#region src/client/http.ts
1287
- /** POST 并解析 body;非 JSON 响应返回 null。 */
1779
+ /** 一次带超时的 fetch;内部 controller,调用方无需传 signal。 */
1780
+ async function fetchWithTimeout(url, init) {
1781
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
1782
+ const timer = controller === null ? null : setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
1783
+ try {
1784
+ return await fetch(url, {
1785
+ ...init,
1786
+ ...controller === null ? {} : { signal: controller.signal }
1787
+ });
1788
+ } finally {
1789
+ if (timer !== null) clearTimeout(timer);
1790
+ }
1791
+ }
1792
+ /**
1793
+ * POST 并解析 body;非 JSON 响应返回 null。
1794
+ *
1795
+ * 这里**故意不**检查 `response.ok`(与 {@link getJson} 不同):host 的写操作错误
1796
+ * 响应体是 `{ok:false, code, error}`,调用方靠 `body.error` 给用户看具体原因
1797
+ * (令牌格式错、额度耗尽、上游超时)。丢掉它就只能显示一句通用的「操作失败」,
1798
+ * 用户无从下手。getJson 可以直接返回 null——读操作没有错误详情可展示。
1799
+ */
1288
1800
  async function postJson(path, payload) {
1289
- return await (await fetch(path, {
1801
+ return await (await fetchWithTimeout(path, {
1290
1802
  method: "POST",
1291
1803
  headers: {
1292
1804
  "content-type": "application/json",
@@ -1298,14 +1810,35 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1298
1810
  }
1299
1811
  /** GET 同源 JSON;非 2xx 或非 JSON 返回 null(调用方决定怎么降级)。 */
1300
1812
  async function getJson(path) {
1301
- const response = await fetch(path, {
1813
+ const response = await fetchWithTimeout(path, {
1302
1814
  headers: { accept: "application/json" },
1303
1815
  cache: "no-store"
1304
1816
  });
1305
1817
  if (!response.ok) return null;
1306
1818
  return await response.json().catch(() => null);
1307
1819
  }
1308
- var init_http = __esmMin((() => {}));
1820
+ /**
1821
+ * 写路径的成败判定收口:失败返回 Error,成功返回 null。
1822
+ *
1823
+ * `postJson` 只把「HTTP/解析失败」折成 `null`,域失败是 `{ok:false, error}`(Host 对
1824
+ * 写操作恒回 HTTP 200,成败看 body)。三个写回调曾各判一次,而**「忘掉令牌」漏判**——
1825
+ * 它直接 `await postJson(...)` 就往下走,于是 Host 侧刻意传播的失败(凭据文件只读时
1826
+ * `ms-auth.forget` 抛错,见其文件头「谎报成功比失败更糟」)在界面上表现为「什么都没
1827
+ * 发生」:没有错误行、busy 清掉、刷新后的快照里令牌还在。判据收在这里一份,三个调用
1828
+ * 方(开关/清单、保存令牌、忘掉令牌)不可能再各自漂移。
1829
+ * @param {ApiBody|null} body - postJson 的返回值。
1830
+ * @param {string} fallback - body 里没有可读原因时的一句话。
1831
+ * @returns {Error|null} 失败原因;`null` = 成功。
1832
+ */
1833
+ function writeFailure(body, fallback) {
1834
+ if (body !== null && body.ok === true) return null;
1835
+ const reason = typeof body?.error === "string" && body.error.trim() !== "" ? body.error : fallback;
1836
+ return new Error(reason);
1837
+ }
1838
+ var FETCH_TIMEOUT_MS;
1839
+ var init_http = __esmMin((() => {
1840
+ FETCH_TIMEOUT_MS = 2e4;
1841
+ }));
1309
1842
 
1310
1843
  //#endregion
1311
1844
  //#region src/client/use-polling-interval.ts
@@ -1382,12 +1915,19 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1382
1915
  const [cadenceMs, setCadenceMs] = useState(defaultCadenceMs);
1383
1916
  /** Host 少给的顶层键(契约漂移);透给面板当 shapeWarning,不静默。 */
1384
1917
  const [missingKeys, setMissingKeys] = useState([]);
1918
+ /**
1919
+ * 每一次轮询尝试都加一,成功与失败都算。`updatedAt` 只在成功时变,而
1920
+ * 面板的 provider 兜底 GET 需要的是「下一轮还会再来」这个信号——快照本身
1921
+ * 失败的那一轮,恰好是兜底路径也应该再试的一轮。
1922
+ */
1923
+ const [pollRevision, setPollRevision] = useState(0);
1385
1924
  const generation = useRef(0);
1386
1925
  const inFlight = useRef(null);
1387
1926
  const cadenceRef = useRef(cadenceMs);
1388
1927
  cadenceRef.current = cadenceMs;
1389
1928
  const load = useCallback(async () => {
1390
1929
  generation.current += 1;
1930
+ setPollRevision((revision) => revision + 1);
1391
1931
  const mine = generation.current;
1392
1932
  const isCurrent = () => generation.current === mine;
1393
1933
  inFlight.current?.abort?.();
@@ -1438,7 +1978,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1438
1978
  loadedOnce,
1439
1979
  updatedAt,
1440
1980
  load,
1441
- missingKeys
1981
+ missingKeys,
1982
+ pollRevision
1442
1983
  };
1443
1984
  }
1444
1985
  var init_use_snapshot_polling = __esmMin((() => {
@@ -1452,7 +1993,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1452
1993
  //#endregion
1453
1994
  //#region src/client/panel-page.ts
1454
1995
  function PanelPage({ tt, localeSubscribe }) {
1455
- const { data, error, loadedOnce, updatedAt, load, missingKeys } = useSnapshotPolling();
1996
+ const { data, error, updatedAt, load, missingKeys, pollRevision } = useSnapshotPolling();
1456
1997
  const [, setLocaleRevision] = useState(0);
1457
1998
  const [openSections, setOpenSections] = useState({
1458
1999
  balance: true,
@@ -1460,6 +2001,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1460
2001
  trend: true,
1461
2002
  events: false,
1462
2003
  provider: true,
2004
+ draw: true,
1463
2005
  token: true
1464
2006
  });
1465
2007
  const [activeTab, setActiveTab] = useState("quota");
@@ -1473,67 +2015,125 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1473
2015
  [key]: !current[key]
1474
2016
  }));
1475
2017
  }, []);
1476
- const { failure, needsSetup, guidance, shapeWarnings } = viewOf(data, error, tt, missingKeys);
1477
- const showSetup = needsSetup && loadedOnce;
2018
+ const { failure, guidance, shapeWarnings } = viewOf(data, error, tt, missingKeys);
1478
2019
  const rawProvider = data?.provider;
1479
2020
  const hasProviderBlock = rawProvider !== void 0 && rawProvider !== null;
1480
2021
  const [fetchedProvider, setFetchedProvider] = useState(null);
1481
- const provider = hasProviderBlock ? providerOf(rawProvider) : fetchedProvider ?? DEGRADED_PROVIDER;
2022
+ const provider = hasProviderBlock ? providerOf$1(rawProvider) : fetchedProvider ?? DEGRADED_PROVIDER;
1482
2023
  useEffect(() => {
1483
2024
  if (hasProviderBlock) return void 0;
1484
2025
  let cancelled = false;
1485
2026
  getJson(PROVIDER_PATH).then((body) => {
1486
- if (!cancelled && body !== null && body.ok === true) setFetchedProvider(providerOf(body));
2027
+ if (!cancelled && body !== null && body.ok === true) setFetchedProvider(providerOf$1(body));
2028
+ }).catch(() => {});
2029
+ return () => {
2030
+ cancelled = true;
2031
+ };
2032
+ }, [hasProviderBlock, pollRevision]);
2033
+ const rawDraw = data?.draw;
2034
+ const hasDrawBlock = rawDraw !== void 0 && rawDraw !== null;
2035
+ const [fetchedDraw, setFetchedDraw] = useState(null);
2036
+ const draw = hasDrawBlock ? drawOf(rawDraw) : fetchedDraw ?? DEGRADED_DRAW;
2037
+ useEffect(() => {
2038
+ if (hasDrawBlock) return void 0;
2039
+ let cancelled = false;
2040
+ getJson(DRAW_PATH).then((body) => {
2041
+ if (!cancelled && body !== null && body.ok === true) setFetchedDraw(drawOf(body));
1487
2042
  }).catch(() => {});
1488
2043
  return () => {
1489
2044
  cancelled = true;
1490
2045
  };
1491
- }, [hasProviderBlock]);
2046
+ }, [hasDrawBlock, pollRevision]);
1492
2047
  const [providerBusy, setProviderBusy] = useState(false);
1493
2048
  const [providerError, setProviderError] = useState(null);
2049
+ const writeErrorText = (reason, t) => reason instanceof Error && reason.name === "AbortError" ? t("common.timeout") : errorText(reason);
2050
+ /**
2051
+ * POST 一个写路径,并把 body 里的域失败抛出去(Host 对写操作恒回 HTTP 200,成败
2052
+ * 看 body)。判据只有 `http.ts#writeFailure` 一份——三个写回调曾各判一次,而
2053
+ * 「忘掉令牌」那条漏了,于是凭据文件只读时界面表现为「什么都没发生」。
2054
+ */
2055
+ const postWrite = useCallback(async (path, payload) => {
2056
+ const failure = writeFailure(await postJson(path, payload), tt("common.httpError"));
2057
+ if (failure !== null) throw failure;
2058
+ }, [tt]);
1494
2059
  const runProviderWrite = useCallback(async (path, payload) => {
1495
2060
  setProviderBusy(true);
1496
2061
  setProviderError(null);
1497
2062
  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");
2063
+ await postWrite(path, payload);
1500
2064
  load();
1501
2065
  } catch (reason) {
1502
- setProviderError(format(tt("provider.error"), { error: errorText(reason) }));
2066
+ setProviderError(format(tt("provider.error"), { error: writeErrorText(reason, tt) }));
1503
2067
  } finally {
1504
2068
  setProviderBusy(false);
1505
2069
  }
1506
- }, [load, tt]);
2070
+ }, [
2071
+ load,
2072
+ postWrite,
2073
+ tt
2074
+ ]);
1507
2075
  const toggleProvider = useCallback((enabled) => void runProviderWrite(PROVIDER_PATH, { enabled }), [runProviderWrite]);
1508
- const saveRoster = useCallback((enabledIds) => void runProviderWrite(PROVIDER_ROSTER_PATH, { enabledIds }), [runProviderWrite]);
2076
+ const saveRoster = useCallback((enabledIds) => {
2077
+ runProviderWrite(PROVIDER_ROSTER_PATH, { enabledIds: normalizeRosterSave(enabledIds) });
2078
+ }, [runProviderWrite]);
1509
2079
  const resetProvider = useCallback(() => void runProviderWrite(PROVIDER_RESET_PATH, {}), [runProviderWrite]);
2080
+ const saveContextWindow = useCallback((contextWindow) => {
2081
+ runProviderWrite(PROVIDER_CONTEXT_PATH, { contextWindow });
2082
+ }, [runProviderWrite]);
2083
+ const [drawBusy, setDrawBusy] = useState(false);
2084
+ const [drawError, setDrawError] = useState(null);
2085
+ const runDrawWrite = useCallback(async (path, payload) => {
2086
+ setDrawBusy(true);
2087
+ setDrawError(null);
2088
+ try {
2089
+ await postWrite(path, payload);
2090
+ load();
2091
+ } catch (reason) {
2092
+ setDrawError(format(tt("draw.error"), { error: writeErrorText(reason, tt) }));
2093
+ } finally {
2094
+ setDrawBusy(false);
2095
+ }
2096
+ }, [
2097
+ load,
2098
+ postWrite,
2099
+ tt
2100
+ ]);
2101
+ const toggleDraw = useCallback((enabled) => void runDrawWrite(DRAW_PATH, { enabled }), [runDrawWrite]);
2102
+ const resetDraw = useCallback(() => void runDrawWrite(DRAW_RESET_PATH, {}), [runDrawWrite]);
1510
2103
  const [tokenBusy, setTokenBusy] = useState(false);
1511
2104
  const [tokenError, setTokenError] = useState(null);
1512
2105
  const saveToken = useCallback(async (value) => {
1513
2106
  setTokenBusy(true);
1514
2107
  setTokenError(null);
1515
2108
  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");
2109
+ await postWrite(TOKEN_PATH, { token: value });
1518
2110
  load();
1519
2111
  } catch (reason) {
1520
- setTokenError(errorText(reason));
2112
+ setTokenError(writeErrorText(reason, tt));
1521
2113
  } finally {
1522
2114
  setTokenBusy(false);
1523
2115
  }
1524
- }, [load]);
2116
+ }, [
2117
+ load,
2118
+ postWrite,
2119
+ tt
2120
+ ]);
1525
2121
  const forgetToken = useCallback(async () => {
1526
2122
  setTokenBusy(true);
1527
2123
  setTokenError(null);
1528
2124
  try {
1529
- await postJson(TOKEN_FORGET_PATH, {});
2125
+ await postWrite(TOKEN_FORGET_PATH, {});
1530
2126
  load();
1531
2127
  } catch (reason) {
1532
- setTokenError(errorText(reason));
2128
+ setTokenError(writeErrorText(reason, tt));
1533
2129
  } finally {
1534
2130
  setTokenBusy(false);
1535
2131
  }
1536
- }, [load]);
2132
+ }, [
2133
+ load,
2134
+ postWrite,
2135
+ tt
2136
+ ]);
1537
2137
  const tokenState = data?.token ?? null;
1538
2138
  const sampleModel = data?.models?.sample?.[0] ?? null;
1539
2139
  const [verifyBusy, setVerifyBusy] = useState(false);
@@ -1545,20 +2145,17 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1545
2145
  setVerifyError(null);
1546
2146
  try {
1547
2147
  const body = await postJson(PROBE_PATH, { modelId });
2148
+ if (body !== null) load();
1548
2149
  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");
2150
+ else setVerifyError(typeof body?.error === "string" ? body.error : tt("common.httpError"));
1550
2151
  } catch (reason) {
1551
- setVerifyError(errorText(reason));
2152
+ setVerifyError(writeErrorText(reason, tt));
1552
2153
  } finally {
1553
2154
  setVerifyBusy(false);
1554
2155
  }
1555
- }, [tt]);
2156
+ }, [load, tt]);
1556
2157
  const quotaBody = () => {
1557
2158
  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
2159
  const snap = data;
1563
2160
  return h("div", null, snap.token?.present === false ? h("div", {
1564
2161
  style: S.formNote,
@@ -1608,9 +2205,23 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1608
2205
  error: providerError,
1609
2206
  onToggle: toggleProvider,
1610
2207
  onSaveList: saveRoster,
2208
+ onSaveContextWindow: saveContextWindow,
1611
2209
  onReset: resetProvider,
1612
2210
  tokenPresent: data?.token?.present === true,
1613
2211
  tt
2212
+ })), h(SectionCard, {
2213
+ title: tt("section.draw"),
2214
+ open: openSections.draw,
2215
+ onToggle: () => toggleSection("draw"),
2216
+ tt
2217
+ }, h(DrawCard, {
2218
+ draw,
2219
+ busy: drawBusy,
2220
+ error: drawError,
2221
+ onToggle: toggleDraw,
2222
+ onReset: resetDraw,
2223
+ tokenPresent: data?.token?.present === true,
2224
+ tt
1614
2225
  })));
1615
2226
  const accessBody = () => h("div", null, h(SectionCard, {
1616
2227
  title: tt("section.token"),
@@ -1688,6 +2299,176 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1688
2299
  init_use_snapshot_polling();
1689
2300
  init_runtime();
1690
2301
  init_styles();
2302
+ init_allow_list();
2303
+ }));
2304
+
2305
+ //#endregion
2306
+ //#region src/client/composer-usage-pill.ts
2307
+ /**
2308
+ * 一个候选自带的 provider,空串算「没有」。
2309
+ *
2310
+ * `typeof provider === "string"` 是不够的判据:`""` 也是字符串。照类型判定会让
2311
+ * `{ provider: "" }` 被当成一个「可用的 next」,于是它遮住仍然可用的 `lastUsed`
2312
+ * ——切模型那一帧胶囊就消失。所以这里判的是**非空字符串**。
2313
+ */
2314
+ function providerOf(candidate) {
2315
+ const provider = candidate?.provider;
2316
+ return typeof provider === "string" && provider !== "" ? provider : void 0;
2317
+ }
2318
+ /**
2319
+ * 会话当前指向的 provider。
2320
+ *
2321
+ * `next` 优先于 `lastUsed`(切换已落地但还没发出去时,选择器显示的就是 `next`),
2322
+ * 但**只在 `next` 真的带了一个非空 provider 时**才优先:picker 刚切完、新选择还没
2323
+ * 落到 `next` 上的那几帧 `next` 是 `{}` 或 `{ provider: "" }`,照「有 next 就用
2324
+ * next」读会得到 `undefined`,于是归属模型在「切换」这个动作本身上把胶囊藏起来。
2325
+ * 两侧都没有可用 provider 时返回 `undefined`——那才是真的判不出来。
2326
+ */
2327
+ function selectedProviderOf(projection) {
2328
+ const source = projection ?? {};
2329
+ return providerOf(source.next) ?? providerOf(source.lastUsed);
2330
+ }
2331
+ /**
2332
+ * 余额读数;`null` 表示**不知道**(没读到 / 上游没给 / 字段不是有限数)。
2333
+ *
2334
+ * 特意不接受 `error` 参数做二次判断:错误文本只影响 tooltip 的措辞,不影响
2335
+ * 「数字可不可信」。把两者揉在一个判定里,会让「上次读到的 143 仍然有效」这种
2336
+ * 情形(`error` 非空但 `available` 有值)被误打成未知。
2337
+ */
2338
+ function availableOf(balance) {
2339
+ const available = balance?.available;
2340
+ return typeof available === "number" && Number.isFinite(available) ? available : null;
2341
+ }
2342
+ /** 今日本地调用次数;`null` 同上,表示读不到(不是 0)。 */
2343
+ function usedTodayOf(snapshot) {
2344
+ const used = snapshot?.quota?.daily?.usedLocal;
2345
+ return typeof used === "number" && Number.isFinite(used) ? used : null;
2346
+ }
2347
+ /** 由余额读数定档;`null`(未读到)与 `0`(读到了、就是零)在此分道。 */
2348
+ function tierOf(available) {
2349
+ if (available === null) return "unknown";
2350
+ return available > 0 ? "ok" : "exhausted";
2351
+ }
2352
+ /** 「余额」段的正文:读不到画「—」+ 单位,读到了画数字 + 单位。 */
2353
+ function balanceText(available, tt) {
2354
+ return `${available === null ? "—" : count(available)} ${tt("composer.balanceUnit")}`;
2355
+ }
2356
+ /** 「今日本地」段的正文:读不到画「—」,读到了画次数。 */
2357
+ function callsText(usedToday, tt) {
2358
+ return format(tt("composer.localCalls"), { calls: usedToday === null ? "—" : count(usedToday) });
2359
+ }
2360
+ /**
2361
+ * 胶囊正文:余额段 + 本地次数段,各带各的口径。两段都必须出现——只画有值的那
2362
+ * 一段会让「哪一段是空的」变成排版事故(读者分不清是没读到还是这一栏根本不在),
2363
+ * 而「—」明确说了是前者。
2364
+ */
2365
+ function readingText(available, usedToday, tt) {
2366
+ return `${balanceText(available, tt)} · ${callsText(usedToday, tt)}`;
2367
+ }
2368
+ /**
2369
+ * 数字是否已经算「上次数据」。
2370
+ *
2371
+ * 两个触发条件各自独立:读失败(`error` 非空,上一轮的数值还留在屏幕上)本身就是
2372
+ * 陈旧;而长时间没有**成功**的读(`updatedAt` 不动)说明定时轮询没在按时回来
2373
+ * ——失败退避或页面被挂起都会这样。取 3 个声明周期而不是 1 个:一次偶发失败就会
2374
+ * 拉长间隔,卡在 1 个周期上会让每一次抖动都把读数打成陈旧的。
2375
+ *
2376
+ * `updatedAt === 0` 表示**从没读到过**,那不是「陈旧」而是「还没有」——调用方据此
2377
+ * 决定不标;本函数只在有读数的前提下判断,故返回 false。
2378
+ */
2379
+ function staleOf(input) {
2380
+ if (input.error !== null && input.error !== void 0) return input.updatedAt > 0;
2381
+ return input.updatedAt > 0 && input.now - input.updatedAt > input.cadenceMs * STALE_CADENCES;
2382
+ }
2383
+ /**
2384
+ * tooltip 的一行:只把**读者下一步会问的**事实拼进来——数字是哪来的(前缀)、
2385
+ * 余额为什么没有(错误原文)、什么时候读的(fetchedAt)、本地计数为什么没有
2386
+ * (quota 那一路的提示)、以及数字是不是旧的。逐段 `filter` 掉空值,不留下
2387
+ * 「 · · 」这样的空槽。
2388
+ */
2389
+ function titleOf(props) {
2390
+ const { tt, balance, usedToday, stale } = props;
2391
+ const parts = [tt("composer.pillTitle")];
2392
+ const failure = balance?.error;
2393
+ parts.push(typeof failure === "string" && failure !== "" ? format(tt("balance.unavailable"), { error: failure }) : null);
2394
+ const fetchedAt = balance?.fetchedAt;
2395
+ parts.push(typeof fetchedAt === "string" && fetchedAt !== "" ? format(tt("balance.fetched"), { time: isoTime(fetchedAt) }) : null);
2396
+ parts.push(usedToday === null ? tt("quota.unavailable") : null);
2397
+ parts.push(stale ? tt("composer.stale") : null);
2398
+ return parts.filter((part) => part !== null).join(" · ");
2399
+ }
2400
+ /**
2401
+ * 内层:只在会话选中本插件的模型时挂载。轮询 hook 在这里,被挂/摘即起停——
2402
+ * 非归属 provider 的会话不发任何请求。
2403
+ */
2404
+ function ModelscopePill(props) {
2405
+ const { tt } = props;
2406
+ const { data, error, loadedOnce, updatedAt } = useSnapshotPolling();
2407
+ const balance = data?.balance ?? null;
2408
+ const available = availableOf(balance);
2409
+ const usedToday = usedTodayOf(data);
2410
+ const cadenceMs = statedCadenceMs(data?.pollSeconds, DEFAULT_CADENCE_MS);
2411
+ const stale = staleOf({
2412
+ error,
2413
+ updatedAt,
2414
+ now: Date.now(),
2415
+ cadenceMs
2416
+ });
2417
+ const pending = !loadedOnce && data === null;
2418
+ const tier = tierOf(available);
2419
+ const title = titleOf({
2420
+ tt,
2421
+ balance,
2422
+ usedToday,
2423
+ stale
2424
+ });
2425
+ return h("button", {
2426
+ type: "button",
2427
+ className: "dsh-modelscope-composer-pill",
2428
+ title,
2429
+ "aria-label": title,
2430
+ style: S.pillTrigger
2431
+ }, h("span", {
2432
+ className: "dsh-modelscope-composer-pill-dot",
2433
+ "aria-hidden": "true",
2434
+ style: {
2435
+ ...S.pillDot,
2436
+ background: TIER_INK[tier]
2437
+ }
2438
+ }), h("span", {
2439
+ className: "dsh-modelscope-composer-pill-plan",
2440
+ style: S.pillPlan
2441
+ }, `${tt("composer.prefix")} · `), h("span", {
2442
+ className: "dsh-modelscope-composer-pill-text",
2443
+ style: tier === "exhausted" ? S.pillTierExhaustedText : void 0
2444
+ }, pending ? "…" : readingText(available, usedToday, tt)), stale ? h("span", {
2445
+ className: "dsh-modelscope-composer-pill-stale",
2446
+ style: S.pillStale
2447
+ }, ` · ${tt("composer.stale")}`) : null);
2448
+ }
2449
+ /**
2450
+ * 外层门控:固定一处调 `useProjection`,再决定内层挂谁。非归属 provider 返回
2451
+ * `null`(不渲染任何东西,也不跑任何请求)。
2452
+ */
2453
+ function ComposerUsagePill(props) {
2454
+ const selection = props.useProjection?.("modelSelection");
2455
+ if (selectedProviderOf(selection) !== "modelscope-token-plan") return null;
2456
+ return h(ModelscopePill, { tt: props.tt });
2457
+ }
2458
+ var MODELSCOPE_PROVIDER, DEFAULT_CADENCE_MS, STALE_CADENCES, TIER_INK;
2459
+ var init_composer_usage_pill = __esmMin((() => {
2460
+ init_runtime();
2461
+ init_styles();
2462
+ init_format();
2463
+ init_use_snapshot_polling();
2464
+ MODELSCOPE_PROVIDER = "modelscope-token-plan";
2465
+ DEFAULT_CADENCE_MS = 3e4;
2466
+ STALE_CADENCES = 3;
2467
+ TIER_INK = {
2468
+ ok: S.pillTierOk,
2469
+ exhausted: S.pillTierExhausted,
2470
+ unknown: S.pillTierUnknown
2471
+ };
1691
2472
  }));
1692
2473
 
1693
2474
  //#endregion
@@ -1732,6 +2513,17 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1732
2513
  } catch (error) {
1733
2514
  console.warn(`[${NS}] config card registration failed:`, error);
1734
2515
  }
2516
+ try {
2517
+ disposers.push(ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
2518
+ name: "conversation.input.right",
2519
+ id: `${NS}-composer-pill`,
2520
+ locale: NS,
2521
+ order: 4,
2522
+ inject: () => ({ tt })
2523
+ }, ComposerUsagePill)));
2524
+ } catch (error) {
2525
+ console.warn(`[${NS}] composer pill registration failed:`, error);
2526
+ }
1735
2527
  ctx.effect(() => () => {
1736
2528
  for (const dispose of disposers.splice(0)) try {
1737
2529
  dispose();
@@ -1743,6 +2535,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1743
2535
  init_const();
1744
2536
  init_i18n();
1745
2537
  init_panel_page();
2538
+ init_composer_usage_pill();
1746
2539
  inject = ["slots", "locale"];
1747
2540
  }));
1748
2541
 
@@ -1758,6 +2551,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1758
2551
  init_styles();
1759
2552
  init_cards();
1760
2553
  init_panel_page();
2554
+ init_composer_usage_pill();
1761
2555
  init_use_polling_interval();
1762
2556
  init_use_snapshot_polling();
1763
2557
  init_http();
@@ -1767,6 +2561,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1767
2561
  const panel = Object.freeze({
1768
2562
  interpretSnapshot,
1769
2563
  viewOf,
2564
+ providerOf: providerOf$1,
1770
2565
  errorOfStatus,
1771
2566
  dictionaries: Object.freeze({
1772
2567
  zh,
@@ -1787,12 +2582,24 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1787
2582
  usePollingInterval,
1788
2583
  useSnapshotPolling,
1789
2584
  getJson,
1790
- postJson
2585
+ postJson,
2586
+ writeFailure,
2587
+ MODELSCOPE_PROVIDER,
2588
+ selectedProviderOf,
2589
+ availableOf,
2590
+ usedTodayOf,
2591
+ tierOf,
2592
+ balanceText,
2593
+ callsText,
2594
+ readingText,
2595
+ staleOf
1791
2596
  }),
1792
2597
  components: Object.freeze({
1793
2598
  BalanceCard,
2599
+ ComposerUsagePill,
1794
2600
  EventsList,
1795
2601
  LocalDailyCard,
2602
+ ModelscopePill,
1796
2603
  ModelUsageTable,
1797
2604
  PanelPage,
1798
2605
  ProviderCard,