dsh-connect-modelscope-token-plan 0.4.0 → 0.5.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
@@ -46,24 +46,65 @@ var dsh_connect_modelscope_token_plan_client = (function() {
46
46
  return slash >= 0 ? id.slice(0, slash) : id;
47
47
  }
48
48
  /**
49
- * 徽章用的窗口短标注(tokens → "128K" / "1M")。
49
+ * 清单行要显示的名字:组头已经把 owner 说过一次,名字若还带着 `owner/` 前缀就切掉
50
+ * ——行宽还给名字本身。
50
51
  *
51
- * K=1024、M=1024²,与档位值(131072…1048576)同一把尺。**不美化**:目录声明的
52
- * 128000 显示成 "125K" 而不是 "128K"——标签必须能从数值反推出来,四舍五入到
53
- * 好看的整数是撒谎。≥1M 用 M(`1.25M` 保留至多一位小数,去掉尾随 .0),
54
- * ≥1024 用 K,更小的原样显示。
52
+ * 为什么需要它:魔搭目录**只回 id**,没有单独的 name 字段,所以 Host 侧
53
+ * `str(entry.name, id)` 的兜底让每一行的 name 就等于 id(见 llm-models.ts)。分组
54
+ * 之后那些前缀与组头逐字重复,一个 34 个模型的清单里同一个 owner 要说十几遍。
55
+ *
56
+ * 只切**与 owner 逐字相同**的前缀:name 若是目录给的人话名字(`DeepSeek V4 Pro`)
57
+ * 或来自别的 owner,原样返回——不按位置硬切,免得把真正的名字切坏。行的 `title`
58
+ * 与复选框的 `aria-label` 仍然用完整 id,所以切掉的信息没有丢。
59
+ * @param {string} id - 完整模型 id(`owner/model`)。
60
+ * @param {string} name - 目录给的名字(通常等于 id)。
61
+ * @returns {string} 行内显示的名字。
62
+ */
63
+ function trimOwnerPrefix(id, name) {
64
+ const prefix = `${catalogOwner(id)}/`;
65
+ return name.startsWith(prefix) ? name.slice(prefix.length) : name;
66
+ }
67
+ /**
68
+ * 徽章用的窗口短标注(tokens → "128K" / "1M")——按**这个数被写出来时用的那个
69
+ * 进制**渲染,不把它换算到另一把尺上。
70
+ *
71
+ * 为什么不「一律除以 1024」:上游自己混用两种写法。档位表与模型的出厂规格是 2 的
72
+ * 幂(131072、262144、1048576),而目录声明与 models.dev / 策展表报的是十进制
73
+ * (128000、200000、256000、1000000——见 `context-catalog.ts` 里逐条抄下来的那些
74
+ * 数)。一律除 1024 会把后一类印成 125K / 195K / 250K / 977K:与上游自己写的名字
75
+ * 全都对不上。LongCat-Flash-Lite 上游就叫 256K,面板印 250K 是另一种撒谎;
76
+ * GLM-5.2 的 1M 印成 977K,读起来像「比档位里的 1M 小一截」——而档位里那个 1M 是
77
+ * 1048576,跟 1000000 差 4.6%,本来就是同一个市场叫法。
78
+ *
79
+ * 判据(与姊妹插件 Agnes 的 `tokenSize` 同一条规则,她那份头注记着同一个坑):
80
+ * 1. ≥ 1M 走 M——十进制的 1,000,000 与二进制的 1,048,576 都读 "1M";
81
+ * 2. 能整除 1000 的按十进制(128000 → "128K"、200000 → "200K");
82
+ * 3. 能整除 1024 的按二进制(131072 → "128K"、262144 → "256K");
83
+ * 4. 两者都不整除的零散声明按十进制取整(123456 → "123K");取整进位到四位数
84
+ * 就交给 M 档(999,999 读 "1M",不读 "1000K")。
85
+ *
86
+ * 代价说在明处:第 2、3 条会**撞车**——128000(两个进制都整除,走十进制)与
87
+ * 131072 都印 "128K"。这是有意的:窗口徽章回答的是「大概能放多少上下文」,而
88
+ * 128K / 256K / 1M 是上游与市场共用的叫法,跟它一致比区分那 2.4% 更有用。
89
+ *
90
+ * `tokens` 由调用方保证是正的有限数(`providerOf` 已挡),所以这里不再兜一次底。
55
91
  * @param {number} tokens - 窗口大小(tokens)。
56
92
  * @returns {string} 短标注。
57
93
  */
58
94
  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`;
95
+ if (tokens >= 1e6) return millionLabel(tokens);
96
+ if (tokens >= 1024) {
97
+ const k = tokens % 1e3 === 0 ? tokens / 1e3 : tokens % 1024 === 0 ? tokens / 1024 : Math.round(tokens / 1e3);
98
+ return k >= 1e3 ? millionLabel(tokens) : `${k}K`;
62
99
  }
63
- if (tokens >= 1024) return `${Math.round(tokens / 1024)}K`;
64
100
  return String(tokens);
65
101
  }
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;
102
+ /** M 档读数:≥10 只留整数,否则至多一位小数(`1048576` → "1M"、`2500000` → "2.5M")。 */
103
+ function millionLabel(tokens) {
104
+ const m = tokens / 1e6;
105
+ return `${m >= 10 || Number.isInteger(m) ? Math.round(m) : Math.round(m * 10) / 10}M`;
106
+ }
107
+ 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, TREND_WINDOW_PATH, MODELSCOPE_TOKEN_URL, MODELSCOPE_USAGE_URL, MODELSCOPE_MODEL_URL_BASE, FEATURED_OWNERS;
67
108
  var init_const = __esmMin((() => {
68
109
  init_wire();
69
110
  NS = "dsh-connect-modelscope-token-plan";
@@ -79,6 +120,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
79
120
  PROVIDER_RESET_PATH = `/api/${NS}/provider/reset`;
80
121
  DRAW_PATH = `/api/${NS}/draw`;
81
122
  DRAW_RESET_PATH = `/api/${NS}/draw/reset`;
123
+ TREND_WINDOW_PATH = `/api/${NS}/trend/window`;
82
124
  MODELSCOPE_TOKEN_URL = "https://modelscope.cn/my/myaccesstoken";
83
125
  MODELSCOPE_USAGE_URL = "https://modelscope.cn/magicube/usage?tab=consume";
84
126
  MODELSCOPE_MODEL_URL_BASE = "https://www.modelscope.cn/models";
@@ -115,7 +157,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
115
157
  "tab.models": "模型",
116
158
  "tab.access": "接入",
117
159
  "section.balance": "魔粒余额(官方数据)",
118
- "section.local": "本地调用(仅经本插件的调用)",
160
+ "section.local": "本地调用(本客户端 · 仅经本插件)",
119
161
  "section.trend": "近 {days} 天本地调用趋势",
120
162
  "section.events": "限流与错误事件",
121
163
  "section.token": "访问令牌",
@@ -129,14 +171,16 @@ var dsh_connect_modelscope_token_plan_client = (function() {
129
171
  "balance.usagePage": "官方用量明细(网页)→",
130
172
  "balance.unavailable": "魔粒余额暂不可读:{error}",
131
173
  "quota.headline": "今日 {calls} 次 · {models} 个模型",
132
- "quota.note": "本地口径:只统计经本插件的调用,直连魔搭的其它客户端不计入;消耗总量以官方魔粒余额为准。",
174
+ "quota.rowCalls": "{calls} 次",
175
+ "quota.rowTokens": "{tokens} token",
176
+ "quota.note": "本地口径:只统计本客户端经本插件的调用(桌面端与 Web 端各记各的);直连魔搭的其它客户端不计入。消耗总量以官方魔粒余额为准。",
133
177
  "quota.unavailable": "本地计数本次读不到(原因见上方提示)",
134
178
  "probe.validity": "验令牌(零额度)",
135
179
  "probe.validOk": "令牌有效(HTTP {status})",
136
180
  "probe.fail": "失败:{error}",
137
181
  "models.viewOnSite": "在魔搭查看 →",
138
182
  "models.viewOnSiteTitle": "打开模型详情页(魔粒单价等只在网页展示)",
139
- "models.featuredTitle": "推荐系列(深度求索 / 智谱 / 通义千问)",
183
+ "models.featuredTitle": "推荐厂商(深度求索 / 智谱 / 通义千问)——它们的模型置顶显示",
140
184
  "provider.featured": "推荐",
141
185
  "section.provider": "接入为 DSH 模型",
142
186
  "provider.enable": "把魔搭模型接入 DSH 模型选择器",
@@ -154,37 +198,56 @@ var dsh_connect_modelscope_token_plan_client = (function() {
154
198
  "provider.reset": "回到默认",
155
199
  "provider.enabledCount": "已启用 {count} / {total}",
156
200
  "provider.rosterHint": "勾选要进入 DSH 模型选择器的模型;不勾 = 全部提供",
201
+ "provider.search": "搜索模型名或 ID",
202
+ "provider.noMatch": "没有匹配的模型。",
203
+ "provider.groupCount": "{count} 个",
204
+ "provider.groupSelected": "已选 {count}",
157
205
  "provider.quotaExhausted": "额度耗尽",
158
206
  "provider.vision": "视觉",
159
207
  "provider.notConfigured": "还没有访问令牌,先到「接入」tab 配置",
160
208
  "provider.source.panel": "面板",
161
209
  "provider.source.config": "配置",
162
210
  "provider.contextWindow": "上下文窗口",
163
- "provider.contextDefault": "默认(跟随目录)",
164
- "provider.contextHint": "目录没声明窗口的模型按此值生效;保存后立即重新注册",
211
+ "provider.contextWindowOverride": "上下文窗口覆盖",
212
+ "provider.contextDefault": "不覆盖(默认)",
213
+ "provider.contextHint": "目录未声明窗口时套用此值(会盖过内置策展表);保存后立即重新注册",
165
214
  "provider.contextCuratedTitle": "窗口来自内置策展表(models.dev 交叉真值),平台未亲口声明;带 * 标记",
166
215
  "section.draw": "出图工具",
167
- "draw.enable": "给 agent 注册 modelscope_draw_image 工具",
216
+ "draw.enable": "开启后,agent 可在对话里用 modelscope_draw_image 生成或编辑图片",
168
217
  "draw.on": "已开启",
169
218
  "draw.off": "未开启",
170
219
  "draw.busy": "切换中…",
171
220
  "draw.mounted": "已注册:{tool}",
172
- "draw.notMounted": "未注册——勾选上方开关即可开启。",
173
- "draw.restartHint": "开关立即保存,但工具的挂载发生在**下次 Host 启动**(工具注册无法撤销)。",
174
- "draw.models": "出图将使用以下模型",
221
+ "draw.notMounted": "未注册——打开上方开关即可开启。",
222
+ "draw.restartHint": "开关立即保存;工具要到下次启动 DSH 才挂载(挂载后无法撤销)。",
223
+ "draw.currentNone": "当前目录里没有可出图的模型——出图会失败(先确认访问令牌已配置、目录已拉到)。",
224
+ "draw.currentSrc.declaredTextToImage": "平台声明为文生图",
225
+ "draw.currentSrc.knownTextToImage": "已知可纯文生图",
226
+ "draw.currentSrc.catalogFirst": "目录里的第一个出图模型",
227
+ "draw.autoHint": "由插件按能力挑;当前会选 {model}",
228
+ "draw.autoHintReason": "由插件按能力挑;当前会选 {model}({reason})",
229
+ "draw.models": "出图模型",
175
230
  "draw.badge": "出图",
176
- "draw.noCandidates": "当前目录里没有出图模型(平台声明 image-to-image / text-to-image 的条目为空);出图不可用。",
231
+ "draw.badge.textToImage": "文生图",
232
+ "draw.badge.imageToImage": "图生图",
233
+ "draw.imageOnlyTitle": "该模型只能图生图/改图:必须通过 imageUrl 传一张输入图,不能纯文生图。",
234
+ "draw.auto": "自动选择",
235
+ "draw.noCandidates": "当前目录里没有可出图的模型,这个工具暂时用不了。",
177
236
  "draw.needsKey": "还没有访问令牌,先到「接入」tab 配置。",
178
237
  "draw.reset": "回到默认",
179
238
  "draw.error": "出图设置保存失败:{error}",
180
239
  "draw.source.panel": "面板",
181
240
  "draw.source.config": "配置",
182
- "draw.absent.noTools": "开关已开,但本机 Host 未提供 agent tools 服务,出图工具缺席(Host 侧限制,非配置问题)。",
241
+ "draw.absent.noTools": "开关已开,但本机没有 tools 服务,工具没挂上(Host 侧限制,不是配置问题)。",
183
242
  "draw.absent.peerLoad": "开关已开,但 tools 模块加载失败,出图工具缺席(Host 问题,非配置)。",
184
243
  "draw.absent.registry": "开关已开,但工具注册被 Host 拒绝,出图工具缺席(Host 问题,非配置)。",
185
244
  "draw.absent.unknown": "开关已开,但出图工具未挂载,原因未知。",
186
245
  "trend.none": "还没有本地调用记录。",
187
246
  "trend.legend": "柱长相对区间内最大值,仅本地口径。",
247
+ "trend.window": "显示天数",
248
+ "trend.windowDefault": "默认(跟随配置)",
249
+ "trend.windowDays": "{days} 天",
250
+ "trend.windowHint": "只决定画多少天;能回看多久另有一个保留窗口(留得比画得多才画得出更远)。",
188
251
  "events.none": "暂无事件。",
189
252
  "events.quota": "额度",
190
253
  "events.rate_limit": "限频",
@@ -236,7 +299,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
236
299
  "tab.models": "Models",
237
300
  "tab.access": "Access",
238
301
  "section.balance": "Magicube balance (official)",
239
- "section.local": "Local calls (only through this plugin)",
302
+ "section.local": "Local calls (this client · only through this plugin)",
240
303
  "section.trend": "Local call trend, last {days} days",
241
304
  "section.events": "Rate-limit and error events",
242
305
  "section.token": "Access token",
@@ -250,14 +313,16 @@ var dsh_connect_modelscope_token_plan_client = (function() {
250
313
  "balance.usagePage": "Official usage details (web) →",
251
314
  "balance.unavailable": "Magicube balance temporarily unreadable: {error}",
252
315
  "quota.headline": "Today {calls} calls · {models} models",
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.",
316
+ "quota.rowCalls": "{calls} calls",
317
+ "quota.rowTokens": "{tokens} tokens",
318
+ "quota.note": "Local scope: only calls this client made through this plugin are counted (desktop and web keep separate ledgers); clients calling ModelScope directly are not. Total consumption is governed by the official Magicube balance.",
254
319
  "quota.unavailable": "Local counters could not be read this time (reason in the notices above)",
255
320
  "probe.validity": "Verify token (free)",
256
321
  "probe.validOk": "Token valid (HTTP {status})",
257
322
  "probe.fail": "Failed: {error}",
258
323
  "models.viewOnSite": "View on ModelScope →",
259
324
  "models.viewOnSiteTitle": "Open the model page (per-model Magicube price is web-only)",
260
- "models.featuredTitle": "Recommended family (DeepSeek / Zhipu / Qwen)",
325
+ "models.featuredTitle": "Recommended vendors (DeepSeek / Zhipu / Qwen) — their models are pinned to the top",
261
326
  "provider.featured": "Recommended",
262
327
  "section.provider": "Register as a DSH model",
263
328
  "provider.enable": "Register ModelScope models in the DSH model picker",
@@ -275,37 +340,56 @@ var dsh_connect_modelscope_token_plan_client = (function() {
275
340
  "provider.reset": "Reset",
276
341
  "provider.enabledCount": "{count} / {total} enabled",
277
342
  "provider.rosterHint": "Tick the models to offer; unticked = offer all",
343
+ "provider.search": "Search model name or ID",
344
+ "provider.noMatch": "No matching models.",
345
+ "provider.groupCount": "{count}",
346
+ "provider.groupSelected": "{count} selected",
278
347
  "provider.quotaExhausted": "Quota exhausted",
279
348
  "provider.vision": "vision",
280
349
  "provider.notConfigured": "No token yet — configure it in the Access tab",
281
350
  "provider.source.panel": "panel",
282
351
  "provider.source.config": "config",
283
352
  "provider.contextWindow": "Context window",
284
- "provider.contextDefault": "Default (catalog)",
285
- "provider.contextHint": "Applies to models whose catalog entry declares no window; re-registers immediately",
353
+ "provider.contextWindowOverride": "Context window override",
354
+ "provider.contextDefault": "No override (default)",
355
+ "provider.contextHint": "Applied when the catalog declares no window (it overrides the built-in curated table); re-registers immediately",
286
356
  "provider.contextCuratedTitle": "Window from the built-in curated table (models.dev cross-check), not declared by the platform; marked with *",
287
357
  "section.draw": "Draw tool",
288
- "draw.enable": "Register the modelscope_draw_image tool for the agent",
358
+ "draw.enable": "Once on, the agent can generate or edit images with modelscope_draw_image",
289
359
  "draw.on": "On",
290
360
  "draw.off": "Off",
291
361
  "draw.busy": "Switching…",
292
362
  "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",
363
+ "draw.notMounted": "Not registered — turn the switch above on.",
364
+ "draw.restartHint": "The switch saves at once; the tool mounts the next time DSH starts (a mount cannot be undone).",
365
+ "draw.currentNone": "No image model in the catalogue right now — drawing will fail (check the access token and that the catalogue has loaded).",
366
+ "draw.currentSrc.declaredTextToImage": "platform-declared text-to-image",
367
+ "draw.currentSrc.knownTextToImage": "known to accept text-to-image",
368
+ "draw.currentSrc.catalogFirst": "first image model in the catalogue",
369
+ "draw.autoHint": "Chosen by the plugin for capability; it would pick {model}",
370
+ "draw.autoHintReason": "Chosen by the plugin for capability; it would pick {model} ({reason})",
371
+ "draw.models": "Image model",
296
372
  "draw.badge": "image",
297
- "draw.noCandidates": "This catalogue declares no image model (no entry with image-to-image / text-to-image); drawing is unavailable.",
373
+ "draw.badge.textToImage": "text-to-image",
374
+ "draw.badge.imageToImage": "image-to-image",
375
+ "draw.imageOnlyTitle": "Image-to-image only: an input image must be passed via imageUrl; pure text-to-image is not supported.",
376
+ "draw.auto": "Auto",
377
+ "draw.noCandidates": "No image model in the catalogue right now, so this tool cannot work yet.",
298
378
  "draw.needsKey": "No token yet — configure it in the Access tab.",
299
379
  "draw.reset": "Reset",
300
380
  "draw.error": "Saving the draw settings failed: {error}",
301
381
  "draw.source.panel": "panel",
302
382
  "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).",
383
+ "draw.absent.noTools": "The switch is on, but this Host has no tools service, so the tool is not mounted (a Host limitation, not a config problem).",
304
384
  "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
385
  "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
386
  "draw.absent.unknown": "The switch is on, but the draw tool is not mounted; the reason is unknown.",
307
387
  "trend.none": "No local call records yet.",
308
388
  "trend.legend": "Bar lengths are relative to the maximum in the range (local scope only).",
389
+ "trend.window": "Days shown",
390
+ "trend.windowDefault": "Default (follow config)",
391
+ "trend.windowDays": "{days} days",
392
+ "trend.windowHint": "Only sets how many days are drawn; how far back history is kept is a separate retention window.",
309
393
  "events.none": "No events.",
310
394
  "events.quota": "Quota",
311
395
  "events.rate_limit": "Rate limit",
@@ -636,6 +720,46 @@ var dsh_connect_modelscope_token_plan_client = (function() {
636
720
  lineHeight: "16px",
637
721
  color: "var(--dsw-alias-label-secondary)"
638
722
  },
723
+ trendChartBars: {
724
+ display: "flex",
725
+ alignItems: "flex-end",
726
+ gap: 2,
727
+ height: 96,
728
+ padding: "0 1px"
729
+ },
730
+ trendChartBar: {
731
+ flex: 1,
732
+ minWidth: 2,
733
+ height: "100%",
734
+ display: "flex",
735
+ alignItems: "flex-end",
736
+ position: "relative"
737
+ },
738
+ trendChartFill: {
739
+ width: "100%",
740
+ minHeight: 2,
741
+ borderRadius: 2,
742
+ background: BRAND
743
+ },
744
+ trendChartValue: {
745
+ position: "absolute",
746
+ left: 0,
747
+ right: 0,
748
+ fontSize: 9,
749
+ lineHeight: 1,
750
+ textAlign: "center",
751
+ color: "var(--dsw-alias-label-secondary)",
752
+ fontVariantNumeric: "tabular-nums",
753
+ pointerEvents: "none"
754
+ },
755
+ trendChartAxis: {
756
+ display: "flex",
757
+ justifyContent: "space-between",
758
+ gap: 12,
759
+ marginTop: 6,
760
+ fontSize: 11,
761
+ color: "var(--dsw-alias-label-secondary)"
762
+ },
639
763
  muted: { color: "var(--dsw-alias-label-secondary)" },
640
764
  error: { color: "var(--dsw-alias-state-error-primary)" },
641
765
  empty: {
@@ -673,6 +797,22 @@ var dsh_connect_modelscope_token_plan_client = (function() {
673
797
  fontSize: 12,
674
798
  margin: "10px 0 0"
675
799
  },
800
+ dotSep: {
801
+ color: "var(--dsw-alias-label-secondary)",
802
+ opacity: .55,
803
+ fontSize: 11
804
+ },
805
+ advancedBlock: {
806
+ marginTop: 14,
807
+ paddingTop: 12,
808
+ borderTop: "1px solid var(--dsw-alias-border-l1)"
809
+ },
810
+ advancedHint: {
811
+ marginTop: 6,
812
+ fontSize: 11,
813
+ lineHeight: "16px",
814
+ color: "var(--dsw-alias-label-secondary)"
815
+ },
676
816
  rosterTools: {
677
817
  display: "flex",
678
818
  gap: 8,
@@ -710,6 +850,27 @@ var dsh_connect_modelscope_token_plan_client = (function() {
710
850
  gap: 8
711
851
  },
712
852
  modelRowOff: { opacity: .55 },
853
+ modelGroup: {
854
+ display: "flex",
855
+ alignItems: "center",
856
+ gap: 6,
857
+ width: "100%",
858
+ padding: "10px 4px 2px",
859
+ background: "none",
860
+ border: "none",
861
+ textAlign: "left",
862
+ cursor: "pointer",
863
+ fontSize: 12,
864
+ fontWeight: 600,
865
+ color: "var(--dsw-alias-label-secondary)"
866
+ },
867
+ modelGroupOwner: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" },
868
+ modelGroupCount: {
869
+ fontWeight: 400,
870
+ opacity: .75,
871
+ fontVariantNumeric: "tabular-nums"
872
+ },
873
+ modelRowGrouped: { paddingLeft: 22 },
713
874
  modelCheck: {
714
875
  flex: "none",
715
876
  width: 15,
@@ -987,13 +1148,25 @@ var dsh_connect_modelscope_token_plan_client = (function() {
987
1148
  const candidates = Array.isArray(d.candidateIds) ? d.candidateIds.map((id) => String(id ?? "")).filter((id) => id !== "") : [];
988
1149
  const reason = d.absentReason;
989
1150
  const absentReason = reason === "no-tools-service" || reason === "peer-load-failed" || reason === "registry-refused" ? reason : null;
1151
+ const rawCandidates = Array.isArray(d.candidates) ? d.candidates.map((entry) => {
1152
+ const row = entry ?? {};
1153
+ return {
1154
+ id: String(row.id ?? ""),
1155
+ capability: row.capability === "text-to-image" ? "text-to-image" : row.capability === "image-to-image" ? "image-to-image" : "other",
1156
+ textToImage: row.textToImage === true
1157
+ };
1158
+ }).filter((row) => row.id !== "") : null;
1159
+ const effectiveSource = d.effectiveSource === "preferred" || d.effectiveSource === "declared-text-to-image" || d.effectiveSource === "known-text-to-image" || d.effectiveSource === "catalog-first" ? d.effectiveSource : null;
990
1160
  return {
991
1161
  enabled: d.enabled === true,
992
1162
  source: d.source === "panel" ? "panel" : "config",
993
1163
  candidateIds: candidates,
994
1164
  candidateCount: typeof d.candidateCount === "number" && Number.isFinite(d.candidateCount) ? d.candidateCount : candidates.length,
1165
+ ...rawCandidates !== null ? { candidates: rawCandidates } : {},
995
1166
  preferredModel: typeof d.preferredModel === "string" && d.preferredModel !== "" ? d.preferredModel : null,
996
1167
  preferredSource: d.preferredSource === "panel" ? "panel" : "config",
1168
+ ..."effectiveModel" in d ? { effectiveModel: typeof d.effectiveModel === "string" && d.effectiveModel !== "" ? d.effectiveModel : null } : {},
1169
+ ..."effectiveSource" in d ? { effectiveSource } : {},
997
1170
  absentReason,
998
1171
  mountPending: d.mountPending === true,
999
1172
  toolName: typeof d.toolName === "string" && d.toolName !== "" ? d.toolName : "modelscope_draw_image"
@@ -1048,8 +1221,11 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1048
1221
  source: "config",
1049
1222
  candidateIds: [],
1050
1223
  candidateCount: 0,
1224
+ candidates: [],
1051
1225
  preferredModel: null,
1052
1226
  preferredSource: "config",
1227
+ effectiveModel: null,
1228
+ effectiveSource: null,
1053
1229
  absentReason: null,
1054
1230
  mountPending: false,
1055
1231
  toolName: "modelscope_draw_image"
@@ -1303,7 +1479,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1303
1479
  }, h("div", { style: S.trendRowHead }, h("span", {
1304
1480
  style: S.trendModel,
1305
1481
  title: row.modelId
1306
- }, row.modelId), h("span", { style: S.trendCredits }, count(row.calls) + (row.tokens !== null ? ` · ${count(row.tokens)}t` : ""))), pct >= 1 ? h("div", {
1482
+ }, row.modelId), h("span", { style: S.trendCredits }, format(tt("quota.rowCalls"), { calls: count(row.calls) }) + (row.tokens !== null ? ` · ${format(tt("quota.rowTokens"), { tokens: count(row.tokens) })}` : ""))), pct >= 1 ? h("div", {
1307
1483
  style: S.trendBar,
1308
1484
  role: "progressbar",
1309
1485
  "aria-valuenow": Math.round(pct),
@@ -1315,7 +1491,42 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1315
1491
  } })) : null);
1316
1492
  }), h("div", { style: S.trendLegend }, tt("trend.legend")));
1317
1493
  }
1318
- /** 近 N 天本地调用趋势(天桶;缺桶 = 该日 0 次,真实零)。 */
1494
+ /**
1495
+ * 轴上的日期短标注:砍掉年份与补零(`2026-09-18` → `9-18`)。
1496
+ *
1497
+ * 轴的跨度最多 trendDays(默认 14,多在同年内),年份在刻度间重复出现,占掉的
1498
+ * 宽度全是从图的呼吸里匀来的;补零的 `09` 同样多占一格而没多给信息。不是
1499
+ * `YYYY-MM-DD` 形状时原样返回——宁可显示一个陌生字符串,也不按位置硬切。
1500
+ * @param {string} key - 天桶 dateKey(本地日期 YYYY-MM-DD)。
1501
+ * @returns {string} 短标注。
1502
+ */
1503
+ function shortDateKey(key) {
1504
+ const value = typeof key === "string" ? key : "";
1505
+ const match = /^\d{4}-(\d{2})-(\d{2})$/.exec(value);
1506
+ return match === null ? value : `${Number(match[1])}-${Number(match[2])}`;
1507
+ }
1508
+ /**
1509
+ * 轴刻度落在哪些桶上(返回下标)。
1510
+ *
1511
+ * 桶数 ≤ {@link MAX_TICKS} 时逐日都标;否则按 `(n-1)/(MAX_TICKS-1)` 等距取点、
1512
+ * 首尾必含——与姊妹插件 Agnes 的用量图同一取法(它 30 天标 7 个)。等距取点是
1513
+ * 对**下标**取整而不是对像素分均,所以刻度与其柱子所属的区间大体对齐,只是轴
1514
+ * 用 `space-between` 铺开;两端的日期恒为真实首末日。
1515
+ * @param {number} count - 桶数。
1516
+ * @returns {number[]} 升序下标。
1517
+ */
1518
+ function tickIndices(count) {
1519
+ if (!Number.isFinite(count) || count <= 0) return [];
1520
+ if (count <= MAX_TICKS) return Array.from({ length: count }, (_, index) => index);
1521
+ const step = (count - 1) / 6;
1522
+ const out = [];
1523
+ for (let tick = 0; tick < MAX_TICKS; tick += 1) {
1524
+ const index = Math.round(tick * step);
1525
+ if (index > (out[out.length - 1] ?? -1)) out.push(index);
1526
+ }
1527
+ return out;
1528
+ }
1529
+ /** 近 N 天本地调用趋势(天桶;缺桶 = 该日 0 次,真实零)——**竖向柱状图**。 */
1319
1530
  function TrendBars({ buckets, tt }) {
1320
1531
  if (!Array.isArray(buckets) || buckets.length === 0) return h("div", { style: S.trendLegend }, tt("trend.none"));
1321
1532
  const rows = buckets.map((bucket) => ({
@@ -1323,20 +1534,93 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1323
1534
  calls: Number.isFinite(bucket.calls) ? Math.max(0, bucket.calls) : 0
1324
1535
  }));
1325
1536
  const max = Math.max(0, ...rows.map((row) => row.calls));
1326
- return h("div", null, rows.map((row) => h("div", {
1327
- key: row.dateKey,
1328
- style: S.trendRowHead
1329
- }, h("span", { style: S.trendModel }, row.dateKey), h("div", {
1537
+ return h("div", null, h("div", { style: S.trendChartBars }, rows.map((row) => {
1538
+ const pct = max > 0 ? row.calls / max * 100 : 0;
1539
+ return h("div", {
1540
+ key: row.dateKey,
1541
+ style: S.trendChartBar,
1542
+ title: `${row.dateKey}: ${count(row.calls)}`
1543
+ }, row.calls > 0 ? h("span", {
1544
+ style: {
1545
+ ...S.trendChartValue,
1546
+ bottom: `calc(${pct}% + 3px)`
1547
+ },
1548
+ "aria-hidden": "true"
1549
+ }, count(row.calls)) : null, h("div", {
1550
+ style: {
1551
+ ...S.trendChartFill,
1552
+ height: `${pct}%`
1553
+ },
1554
+ role: "progressbar",
1555
+ "aria-label": `${row.dateKey} ${count(row.calls)}`,
1556
+ "aria-valuenow": Math.round(pct),
1557
+ "aria-valuemin": 0,
1558
+ "aria-valuemax": 100
1559
+ }));
1560
+ })), h("div", { style: S.trendChartAxis }, tickIndices(rows.length).map((index) => {
1561
+ const key = rows[index]?.dateKey ?? "";
1562
+ return h("span", {
1563
+ key,
1564
+ title: key,
1565
+ style: { whiteSpace: "nowrap" }
1566
+ }, shortDateKey(key));
1567
+ })), h("div", { style: S.trendLegend }, tt("trend.legend")));
1568
+ }
1569
+ /**
1570
+ * 趋势显示窗口选择器:默认(跟随 `trendDays` 配置)/ 7 / 14 / 30 / 90 天。
1571
+ *
1572
+ * 它改的是**画多少天**,不是**留多少天**(保留窗口是 host-config 的
1573
+ * `retentionDays`)——所以档位只列到 `trend.maxDays` 为止:画得比留得多会把已剪的
1574
+ * 日子画成 0,那个组合在 Host 侧已被夹掉,这里也不给它入口。
1575
+ *
1576
+ * 保存成功后由调用方 `load()` 回显 Host 落盘的值(不做乐观更新,与 provider /
1577
+ * draw 的写路径同一纪律)。当前值不在档位里(例如手动改过 usage-prefs.json)时
1578
+ * value 与任何 option 都不匹配 → 浏览器显示第一项,但下一发 onChange 仍发正确的
1579
+ * 值——与上下文窗口选择器同一处可接受的边缘表现,不为它造「自定义」档位。
1580
+ */
1581
+ function TrendWindowPicker({ trend, busy, error, onChange, tt }) {
1582
+ if (trend === void 0) return null;
1583
+ const maxDays = typeof trend.maxDays === "number" && Number.isFinite(trend.maxDays) && trend.maxDays > 0 ? Math.floor(trend.maxDays) : 0;
1584
+ const tiers = TREND_WINDOW_TIERS.filter((tier) => maxDays === 0 || tier <= maxDays);
1585
+ const current = trend.windowDays;
1586
+ return h("div", { style: {
1587
+ display: "flex",
1588
+ gap: 8,
1589
+ alignItems: "center",
1590
+ flexWrap: "wrap",
1591
+ marginBottom: 10
1592
+ } }, h("label", {
1330
1593
  style: {
1331
- ...S.trendBar,
1332
- flex: "1",
1333
- marginLeft: 10
1594
+ fontSize: 12,
1595
+ color: "var(--dsw-alias-label-secondary)"
1334
1596
  },
1335
- "aria-hidden": "true"
1336
- }, max > 0 && row.calls > 0 ? h("div", { style: {
1337
- ...S.barFill,
1338
- width: `${row.calls / max * 100}%`
1339
- } }) : null), h("span", { style: S.quotaUsed }, String(row.calls)))), h("div", { style: S.trendLegend }, tt("trend.legend")));
1597
+ htmlFor: "trend-window"
1598
+ }, tt("trend.window")), h("select", {
1599
+ id: "trend-window",
1600
+ style: {
1601
+ ...S.rosterBulk,
1602
+ height: 28,
1603
+ width: "auto"
1604
+ },
1605
+ disabled: busy,
1606
+ value: current === null || current === void 0 ? "" : String(current),
1607
+ onChange: (event) => {
1608
+ const value = String(event.target?.value ?? "");
1609
+ onChange(value === "" ? null : Number(value));
1610
+ }
1611
+ }, h("option", {
1612
+ value: "",
1613
+ key: "default"
1614
+ }, tt("trend.windowDefault")), tiers.map((tier) => h("option", {
1615
+ value: String(tier),
1616
+ key: String(tier)
1617
+ }, format(tt("trend.windowDays"), { days: tier })))), h("span", { style: {
1618
+ ...S.muted,
1619
+ fontSize: 11
1620
+ } }, tt("trend.windowHint")), error !== null ? h("span", {
1621
+ style: S.formError,
1622
+ role: "alert"
1623
+ }, error) : null);
1340
1624
  }
1341
1625
  /** 事件流(429/错误),Newest-first。 */
1342
1626
  function EventsList({ events, tt }) {
@@ -1402,6 +1686,33 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1402
1686
  }, tt("token.link")));
1403
1687
  }
1404
1688
  /**
1689
+ * 按 owner 把清单分组,**推荐的三家置顶**(组内保持目录首见序)。
1690
+ *
1691
+ * 分组的理由只有一个:`owner/model` 的 owner 前缀在每一行里重复,而面板一屏能放
1692
+ * 十几行——同一个 owner 说十几遍既占宽度又不提供任何信息。置顶沿用「推荐」徽章的
1693
+ * 同一份 `FEATURED_OWNERS`,两处不可能对「哪三家值得置顶」说两套话。
1694
+ * @param {RosterRow[]} rows - 归一后的清单行。
1695
+ * @returns {{owner: string, rows: RosterRow[]}[]} 分组(置顶 owner 在前)。
1696
+ */
1697
+ function groupByOwner(rows) {
1698
+ const order = [];
1699
+ const byOwner = /* @__PURE__ */ new Map();
1700
+ for (const row of rows) {
1701
+ const owner = catalogOwner(row.id);
1702
+ const bucket = byOwner.get(owner);
1703
+ if (bucket === void 0) {
1704
+ byOwner.set(owner, [row]);
1705
+ order.push(owner);
1706
+ } else bucket.push(row);
1707
+ }
1708
+ const featured = FEATURED_OWNERS.filter((owner) => byOwner.has(owner));
1709
+ const rest = order.filter((owner) => !FEATURED_OWNERS.includes(owner));
1710
+ return [...featured, ...rest].map((owner) => ({
1711
+ owner,
1712
+ rows: byOwner.get(owner) ?? []
1713
+ }));
1714
+ }
1715
+ /**
1405
1716
  * 接入为 DSH 模型的区块(开关 + 注册状态 + 允许清单)。
1406
1717
  *
1407
1718
  * 这是 M4 把抽象「试调」升级成真接入的落点:开关翻转即 POST provider 路由,
@@ -1432,6 +1743,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1432
1743
  const hostIds = status.enabledIds;
1433
1744
  const hostKey = useMemo(() => JSON.stringify(hostIds), [hostIds]);
1434
1745
  const [draft, setDraft] = useState(() => hostIds.slice());
1746
+ const [search, setSearch] = useState("");
1747
+ const [collapsed, setCollapsed] = useState([]);
1435
1748
  const hideAllMode = parseAllowedList(draft).mode === "none";
1436
1749
  useEffect(() => {
1437
1750
  setDraft(normalizeRosterSave(hostIds));
@@ -1444,30 +1757,20 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1444
1757
  return kept.includes(id) ? kept.filter((x) => x !== id) : [...kept, id];
1445
1758
  });
1446
1759
  const ticked = hideAllMode ? 0 : roster.filter((row) => draft.includes(row.id)).length;
1447
- const degraded = status.error === "unavailable";
1448
- let statusNode;
1449
- if (!degraded && typeof status.error === "string" && status.error !== "") statusNode = h("div", {
1760
+ const alertNode = !(status.error === "unavailable") && typeof status.error === "string" && status.error !== "" ? h("div", {
1450
1761
  style: S.formError,
1451
1762
  role: "alert"
1452
- }, format(tt("provider.error"), { error: status.error }));
1453
- else if (status.registered === true) statusNode = h("div", {
1454
- style: {
1455
- fontSize: 12,
1456
- color: "var(--dsw-alias-label-secondary)"
1457
- },
1458
- role: "status"
1459
- }, tt("provider.registered"));
1460
- else if (status.llmAvailable !== true) statusNode = h("div", { style: {
1461
- ...S.formNote,
1462
- color: "var(--dsw-alias-state-warn-primary)"
1463
- } }, tt("provider.llmMissing"));
1464
- else statusNode = h("div", {
1465
- style: {
1466
- ...S.muted,
1467
- fontSize: 12
1468
- },
1469
- role: "status"
1470
- }, tt("provider.notRegistered"));
1763
+ }, format(tt("provider.error"), { error: status.error })) : null;
1764
+ const statusPhrase = alertNode !== null ? null : status.registered === true ? {
1765
+ key: "provider.registered",
1766
+ warn: false
1767
+ } : status.llmAvailable !== true ? {
1768
+ key: "provider.llmMissing",
1769
+ warn: true
1770
+ } : {
1771
+ key: "provider.notRegistered",
1772
+ warn: false
1773
+ };
1471
1774
  const tokenHint = enabled && tokenPresent === false ? h("div", { style: {
1472
1775
  ...S.formNote,
1473
1776
  color: "var(--dsw-alias-state-warn-primary)"
@@ -1491,19 +1794,18 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1491
1794
  label: "1M"
1492
1795
  }
1493
1796
  ];
1494
- const contextNode = onSaveContextWindow === void 0 ? null : h("div", { style: {
1797
+ const contextNode = onSaveContextWindow === void 0 ? null : h("div", { style: S.advancedBlock }, h("div", { style: {
1495
1798
  display: "flex",
1496
1799
  gap: 8,
1497
1800
  alignItems: "center",
1498
- flexWrap: "wrap",
1499
- margin: "0 0 8px"
1801
+ flexWrap: "wrap"
1500
1802
  } }, h("label", {
1501
1803
  style: {
1502
1804
  fontSize: 12,
1503
1805
  color: "var(--dsw-alias-label-secondary)"
1504
1806
  },
1505
1807
  htmlFor: "provider-context-window"
1506
- }, tt("provider.contextWindow")), h("select", {
1808
+ }, tt("provider.contextWindowOverride")), h("select", {
1507
1809
  id: "provider-context-window",
1508
1810
  style: {
1509
1811
  ...S.rosterBulk,
@@ -1521,13 +1823,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1521
1823
  }, tt("provider.contextDefault")), CONTEXT_TIERS.map((tier) => h("option", {
1522
1824
  value: String(tier.value),
1523
1825
  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")));
1826
+ }, tier.label)))), h("div", { style: S.advancedHint }, tt("provider.contextHint")));
1531
1827
  const switchRow = h("div", { style: {
1532
1828
  display: "flex",
1533
1829
  gap: 10,
@@ -1541,57 +1837,35 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1541
1837
  label: tt(enabled ? "provider.on" : "provider.off"),
1542
1838
  busyLabel: tt("provider.busy"),
1543
1839
  title: tt("provider.enable")
1544
- }), h("span", { style: {
1840
+ }), statusPhrase === null ? null : h("span", {
1841
+ style: S.dotSep,
1842
+ "aria-hidden": "true"
1843
+ }, "·"), statusPhrase === null ? null : h("span", {
1844
+ role: "status",
1845
+ style: {
1846
+ fontSize: 12,
1847
+ color: statusPhrase.warn ? "var(--dsw-alias-state-warn-primary)" : "var(--dsw-alias-label-secondary)"
1848
+ }
1849
+ }, tt(statusPhrase.key)), h("span", {
1850
+ style: S.dotSep,
1851
+ "aria-hidden": "true"
1852
+ }, "·"), h("span", { style: {
1545
1853
  fontSize: 11,
1546
1854
  color: "var(--dsw-alias-label-secondary)"
1547
1855
  } }, tt(sourceKey)));
1548
- const rosterBlock = roster.length === 0 ? h("div", { style: {
1549
- ...S.muted,
1550
- fontSize: 12,
1551
- marginTop: 6
1552
- } }, tt("provider.rosterHint")) : h("div", { style: S.modelPanel }, h("div", { style: S.rosterTools }, h("span", {
1553
- style: S.rosterCount,
1554
- title: format(tt("provider.enabledCount"), {
1555
- count: ticked,
1556
- total: roster.length
1557
- })
1558
- }, format(tt("provider.enabledCount"), {
1559
- count: ticked,
1560
- total: roster.length
1561
- })), h("button", {
1562
- type: "button",
1563
- style: S.rosterBulk,
1564
- disabled: busy,
1565
- onClick: () => {
1566
- setDraft(rosterIds.slice());
1567
- onSaveList(rosterIds.slice());
1568
- }
1569
- }, tt("provider.allowAll")), h("button", {
1570
- type: "button",
1571
- style: S.rosterBulk,
1572
- disabled: busy,
1573
- onClick: () => {
1574
- const none = hideAllRoster();
1575
- setDraft(none);
1576
- onSaveList(none);
1577
- }
1578
- }, tt("provider.hideAll")), h("button", {
1579
- type: "button",
1580
- style: S.primary,
1581
- disabled: busy,
1582
- onClick: () => onSaveList(draft.slice())
1583
- }, tt("provider.saveList"))), h("ul", {
1584
- style: S.modelList,
1585
- role: "list"
1586
- }, roster.map((row) => {
1856
+ const renderRosterRow = (row) => {
1587
1857
  const id = row.id;
1588
1858
  const unusable = row.available === false || row.quotaExhausted === true;
1589
1859
  return h("li", {
1590
1860
  key: id,
1591
1861
  style: unusable ? {
1592
1862
  ...S.modelRow,
1863
+ ...S.modelRowGrouped,
1593
1864
  ...S.modelRowOff
1594
- } : S.modelRow
1865
+ } : {
1866
+ ...S.modelRow,
1867
+ ...S.modelRowGrouped
1868
+ }
1595
1869
  }, h("div", { style: S.modelRowHead }, h("label", { style: {
1596
1870
  display: "flex",
1597
1871
  alignItems: "center",
@@ -1609,39 +1883,128 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1609
1883
  }), h("span", {
1610
1884
  style: S.modelName,
1611
1885
  title: id
1612
- }, row.name)), row.vision === true ? h("span", { style: S.modelBadge }, tt("provider.vision")) : null, typeof row.contextWindow === "number" ? h("span", {
1886
+ }, trimOwnerPrefix(id, row.name))), row.vision === true ? h("span", { style: S.modelBadge }, tt("provider.vision")) : null, typeof row.contextWindow === "number" ? h("span", {
1613
1887
  style: row.contextSource === "curated" ? {
1614
1888
  ...S.modelBadge,
1615
1889
  color: "var(--dsw-alias-label-secondary)"
1616
1890
  } : S.modelBadge,
1617
1891
  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", {
1619
- style: S.modelBadgeFeatured,
1620
- title: tt("models.featuredTitle")
1621
- }, tt("provider.featured")) : null, row.quotaExhausted === true ? h("span", { style: {
1892
+ }, row.contextSource === "curated" ? formatWindow(row.contextWindow) + " *" : formatWindow(row.contextWindow)) : null, row.quotaExhausted === true ? h("span", { style: {
1622
1893
  ...S.modelBadge,
1623
1894
  color: "var(--dsw-alias-state-error-primary)"
1624
1895
  } }, tt("provider.quotaExhausted")) : null, h("a", {
1625
1896
  style: {
1626
1897
  ...S.formNote,
1627
1898
  margin: 0,
1628
- marginLeft: "auto"
1899
+ marginLeft: "auto",
1900
+ whiteSpace: "nowrap"
1629
1901
  },
1630
1902
  href: modelscopeModelUrl(row.id),
1631
1903
  target: "_blank",
1632
1904
  rel: "noreferrer",
1633
1905
  title: tt("models.viewOnSiteTitle")
1634
1906
  }, tt("models.viewOnSite"))));
1635
- })), h("div", { style: S.rosterFoot }, h("button", {
1907
+ };
1908
+ const query = search.trim().toLowerCase();
1909
+ const visibleRows = query === "" ? roster : roster.filter((row) => `${row.id} ${String(row.name ?? "")}`.toLowerCase().includes(query));
1910
+ const ownerGroups = groupByOwner(visibleRows);
1911
+ const rosterBlock = roster.length === 0 ? h("div", { style: {
1912
+ ...S.muted,
1913
+ fontSize: 12,
1914
+ marginTop: 6
1915
+ } }, tt("provider.rosterHint")) : h("div", { style: S.modelPanel }, h("div", { style: S.rosterTools }, h("input", {
1916
+ type: "search",
1917
+ name: "provider-model-search",
1918
+ autoComplete: "off",
1919
+ style: {
1920
+ ...S.input,
1921
+ flex: "1 1 160px",
1922
+ minWidth: 0
1923
+ },
1924
+ placeholder: tt("provider.search"),
1925
+ "aria-label": tt("provider.search"),
1926
+ value: search,
1927
+ disabled: busy,
1928
+ onChange: (event) => setSearch(String(event.target?.value ?? ""))
1929
+ }), h("span", {
1930
+ style: S.rosterCount,
1931
+ title: format(tt("provider.enabledCount"), {
1932
+ count: ticked,
1933
+ total: roster.length
1934
+ })
1935
+ }, format(tt("provider.enabledCount"), {
1936
+ count: ticked,
1937
+ total: roster.length
1938
+ })), h("button", {
1939
+ type: "button",
1940
+ style: S.rosterBulk,
1941
+ disabled: busy,
1942
+ onClick: () => {
1943
+ setDraft(rosterIds.slice());
1944
+ onSaveList(rosterIds.slice());
1945
+ }
1946
+ }, tt("provider.allowAll")), h("button", {
1947
+ type: "button",
1948
+ style: S.rosterBulk,
1949
+ disabled: busy,
1950
+ onClick: () => {
1951
+ const none = hideAllRoster();
1952
+ setDraft(none);
1953
+ onSaveList(none);
1954
+ }
1955
+ }, tt("provider.hideAll")), h("button", {
1956
+ type: "button",
1957
+ style: S.primary,
1958
+ disabled: busy,
1959
+ onClick: () => onSaveList(draft.slice())
1960
+ }, tt("provider.saveList"))), visibleRows.length === 0 ? h("div", { style: {
1961
+ ...S.muted,
1962
+ fontSize: 12,
1963
+ padding: "10px 4px"
1964
+ } }, tt("provider.noMatch")) : h("ul", {
1965
+ style: S.modelList,
1966
+ role: "list"
1967
+ }, ownerGroups.map((group) => {
1968
+ const isCollapsed = query === "" && collapsed.includes(group.owner);
1969
+ const groupTicked = hideAllMode ? 0 : group.rows.filter((row) => draft.includes(row.id)).length;
1970
+ return h("li", { key: group.owner }, h("button", {
1971
+ type: "button",
1972
+ style: S.modelGroup,
1973
+ "aria-expanded": !isCollapsed,
1974
+ "aria-label": `${tt(isCollapsed ? "section.expand" : "section.collapse")}: ${group.owner}`,
1975
+ onClick: () => setCollapsed((current) => current.includes(group.owner) ? current.filter((owner) => owner !== group.owner) : [...current, group.owner])
1976
+ }, h("svg", {
1977
+ viewBox: "0 0 16 16",
1978
+ width: 12,
1979
+ height: 12,
1980
+ fill: "none",
1981
+ stroke: "currentColor",
1982
+ strokeWidth: "1.5",
1983
+ strokeLinecap: "round",
1984
+ strokeLinejoin: "round",
1985
+ "aria-hidden": "true",
1986
+ style: isCollapsed ? {
1987
+ ...S.chevron,
1988
+ transform: "rotate(-90deg)"
1989
+ } : S.chevron
1990
+ }, h("path", { d: "M3 6l5 5 5-5" })), h("span", { style: S.modelGroupOwner }, group.owner), FEATURED_OWNERS.includes(group.owner) ? h("span", {
1991
+ style: S.modelBadgeFeatured,
1992
+ title: tt("models.featuredTitle")
1993
+ }, tt("provider.featured")) : null, h("span", { style: S.modelGroupCount }, format(tt("provider.groupCount"), { count: group.rows.length })), groupTicked > 0 ? h("span", { style: S.modelGroupCount }, format(tt("provider.groupSelected"), { count: groupTicked })) : null), isCollapsed ? null : h("ul", {
1994
+ style: S.modelList,
1995
+ role: "list"
1996
+ }, group.rows.map(renderRosterRow)));
1997
+ })));
1998
+ const resetRow = h("div", { style: S.rosterFoot }, h("button", {
1636
1999
  type: "button",
1637
2000
  style: S.button,
1638
2001
  disabled: busy,
1639
2002
  onClick: onReset
1640
- }, tt("provider.reset"))));
1641
- return h("div", null, switchRow, statusNode, tokenHint, contextNode, h("div", { style: {
2003
+ }, tt("provider.reset")));
2004
+ return h("div", null, switchRow, alertNode, tokenHint, h("div", { style: {
1642
2005
  ...S.sectionTitle,
1643
2006
  margin: "18px 0 8px"
1644
- } }, tt("provider.models")), rosterBlock, error !== null ? h("div", {
2007
+ } }, tt("provider.models")), rosterBlock, contextNode, resetRow, error !== null ? h("div", {
1645
2008
  style: S.formError,
1646
2009
  role: "alert"
1647
2010
  }, error) : null);
@@ -1660,11 +2023,15 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1660
2023
  * @param {object} props - 渲染参数。
1661
2024
  * @returns {unknown} 卡片节点。
1662
2025
  */
1663
- function DrawCard({ draw, busy, error, onToggle, onReset, tokenPresent, tt }) {
2026
+ function DrawCard({ draw, busy, error, onToggle, onReset, onSelectModel, onAutoModel, tokenPresent, tt }) {
1664
2027
  const status = drawOf(draw);
1665
2028
  const enabled = status.enabled === true;
1666
2029
  const candidates = status.candidateIds;
1667
2030
  const sourceKey = status.source === "panel" ? "draw.source.panel" : "draw.source.config";
2031
+ const effectiveModel = status.effectiveModel;
2032
+ const hasEffectiveKey = effectiveModel !== void 0;
2033
+ const candidateDetails = status.candidates;
2034
+ const autoReasonKey = status.effectiveSource === "declared-text-to-image" ? "draw.currentSrc.declaredTextToImage" : status.effectiveSource === "known-text-to-image" ? "draw.currentSrc.knownTextToImage" : status.effectiveSource === "catalog-first" ? "draw.currentSrc.catalogFirst" : null;
1668
2035
  const switchRow = h("div", { style: {
1669
2036
  display: "flex",
1670
2037
  gap: 10,
@@ -1715,32 +2082,113 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1715
2082
  ...S.formNote,
1716
2083
  color: "var(--dsw-alias-state-warn-primary)"
1717
2084
  } }, tt("draw.needsKey")) : null;
1718
- const candidateBlock = candidates.length === 0 ? h("div", { style: {
2085
+ const autoDescription = !hasEffectiveKey || status.preferredModel !== null ? null : effectiveModel === null || effectiveModel === "" ? h("div", { style: {
1719
2086
  ...S.muted,
1720
- fontSize: 12,
1721
- marginTop: 6
1722
- } }, tt("draw.noCandidates")) : h("div", { style: {
2087
+ fontSize: 11,
2088
+ lineHeight: "16px",
2089
+ paddingLeft: 25
2090
+ } }, tt("draw.currentNone")) : h("div", { style: {
2091
+ ...S.muted,
2092
+ fontSize: 11,
2093
+ lineHeight: "16px",
2094
+ paddingLeft: 25
2095
+ } }, autoReasonKey === null ? format(tt("draw.autoHint"), { model: effectiveModel }) : format(tt("draw.autoHintReason"), {
2096
+ model: effectiveModel,
2097
+ reason: tt(autoReasonKey)
2098
+ }));
2099
+ const RADIO_NAME = "modelscope-draw-model";
2100
+ /**
2101
+ * 一个候选的能力徽章(**正向各给一枚**,不是「能力减项 + 警告」)。
2102
+ *
2103
+ * 之前写成「平台能力档一枚 + 不能纯文生图时再加警告」是不对称的:能吃纯文生图
2104
+ * 是**加项优势**,却在 UI 上完全不可见,于是两个 Qwen-Image-Edit 看起来一样,
2105
+ * 其中一个还莫名带着警告。现在两个事实各画一枚:
2106
+ * - `图生图`——平台声明的 image-to-image(改图/图生图);
2107
+ * - `文生图`——能吃纯文生图(平台声明的 text-to-image,或实测已知的那个 id)。
2108
+ * 只给「图生图」的模型,徽章上挂 title 说明它必须给输入图(把警告从第二枚
2109
+ * 徽章降级成悬停说明,列表不再被警告噪声占满)。
2110
+ * 旧 Host 没有候选明细时退化为通用「出图」徽章——不猜能力。
2111
+ */
2112
+ const capabilityBadges = (row) => {
2113
+ if (row.capability === null) return [h("span", {
2114
+ key: "badge",
2115
+ style: S.modelBadge
2116
+ }, tt("draw.badge"))];
2117
+ const badges = [];
2118
+ if (row.capability === "image-to-image") badges.push(h("span", {
2119
+ key: "badge-i2i",
2120
+ style: S.modelBadge,
2121
+ title: row.textToImage === true ? void 0 : tt("draw.imageOnlyTitle")
2122
+ }, tt("draw.badge.imageToImage")));
2123
+ if (row.textToImage === true) badges.push(h("span", {
2124
+ key: "badge-t2i",
2125
+ style: S.modelBadge
2126
+ }, tt("draw.badge.textToImage")));
2127
+ if (badges.length === 0) badges.push(h("span", {
2128
+ key: "badge",
2129
+ style: S.modelBadge
2130
+ }, tt("draw.badge")));
2131
+ return badges;
2132
+ };
2133
+ const candidateRows = Array.isArray(candidateDetails) ? candidateDetails.map((row) => ({
2134
+ id: row.id,
2135
+ capability: row.capability,
2136
+ textToImage: row.textToImage
2137
+ })) : candidates.map((id) => ({
2138
+ id,
2139
+ capability: null,
2140
+ textToImage: null
2141
+ }));
2142
+ /**
2143
+ * 一行广播的骨架:单选框 + 名字(+ 其余徽章由调用方给),可选第二行说明。
2144
+ *
2145
+ * `description` 是「这一项是什么」的地方(只有「自动选择」需要),缩进对齐到
2146
+ * 文字起点(复选框 15px + gap 10px = 25px),读起来是该项的注脚而不是新一行。
2147
+ */
2148
+ const radioRow = (key, checked, label, labelText, disabled, onChange, extra, description) => h("li", {
2149
+ key,
2150
+ style: S.modelRow
2151
+ }, h("div", { style: S.modelRowHead }, h("label", { style: {
1723
2152
  display: "flex",
1724
- flexDirection: "column",
1725
- gap: 4
1726
- } }, candidates.map((id) => h("div", {
1727
- key: id,
1728
- style: S.modelRowHead
1729
- }, h("code", {
2153
+ alignItems: "center",
2154
+ gap: 10,
2155
+ flex: "1 1 auto",
2156
+ minWidth: 0,
2157
+ cursor: disabled ? "default" : "pointer"
2158
+ } }, h("input", {
2159
+ type: "radio",
2160
+ name: RADIO_NAME,
2161
+ checked,
2162
+ disabled,
2163
+ onChange,
2164
+ style: S.modelCheck,
2165
+ "aria-label": label
2166
+ }), h("span", {
1730
2167
  style: S.modelName,
1731
- title: id
1732
- }, id), h("span", { style: S.modelBadge }, tt("draw.badge")), h("a", {
2168
+ title: label
2169
+ }, labelText)), ...extra), ...description === void 0 ? [] : [description]);
2170
+ const candidateBlock = candidateRows.length === 0 ? h("div", { style: {
2171
+ ...S.muted,
2172
+ fontSize: 12,
2173
+ marginTop: 6
2174
+ } }, tt("draw.noCandidates")) : h("div", { style: S.modelPanel }, h("ul", {
2175
+ style: S.modelList,
2176
+ role: "radiogroup",
2177
+ "aria-label": tt("draw.models")
2178
+ }, radioRow("__auto__", status.preferredModel === null, tt("draw.auto"), tt("draw.auto"), busy, () => onAutoModel(), [], autoDescription === null ? void 0 : autoDescription), ...candidateRows.map((row) => radioRow(row.id, status.preferredModel === row.id, row.id, row.id, busy, () => onSelectModel(row.id), [...capabilityBadges(row), h("a", {
2179
+ key: "link",
1733
2180
  style: {
1734
2181
  ...S.formNote,
1735
2182
  margin: 0,
1736
- marginLeft: "auto"
2183
+ marginLeft: "auto",
2184
+ whiteSpace: "nowrap"
1737
2185
  },
1738
- href: modelscopeModelUrl(id),
2186
+ href: modelscopeModelUrl(row.id),
1739
2187
  target: "_blank",
1740
2188
  rel: "noreferrer",
1741
2189
  title: tt("models.viewOnSiteTitle")
1742
- }, tt("models.viewOnSite")))));
1743
- return h("div", null, switchRow, statusNode, restartHint, tokenHint, h("div", { style: {
2190
+ }, tt("models.viewOnSite"))]))));
2191
+ return h("div", null, switchRow, statusNode, h("div", { style: {
1744
2192
  ...S.sectionTitle,
1745
2193
  margin: "18px 0 8px"
1746
2194
  } }, tt("draw.models")), candidateBlock, h("div", { style: S.rosterFoot }, h("button", {
@@ -1751,9 +2199,9 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1751
2199
  }, tt("draw.reset"))), error !== null ? h("div", {
1752
2200
  style: S.formError,
1753
2201
  role: "alert"
1754
- }, error) : null);
2202
+ }, error) : null, restartHint, tokenHint);
1755
2203
  }
1756
- var EVENT_KIND_KEY, DRAW_ABSENT_KEY;
2204
+ var MAX_TICKS, TREND_WINDOW_TIERS, EVENT_KIND_KEY, DRAW_ABSENT_KEY;
1757
2205
  var init_cards = __esmMin((() => {
1758
2206
  init_format();
1759
2207
  init_runtime();
@@ -1762,6 +2210,13 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1762
2210
  init_snapshot();
1763
2211
  init_toggle_switch();
1764
2212
  init_allow_list();
2213
+ MAX_TICKS = 7;
2214
+ TREND_WINDOW_TIERS = [
2215
+ 7,
2216
+ 14,
2217
+ 30,
2218
+ 90
2219
+ ];
1765
2220
  EVENT_KIND_KEY = {
1766
2221
  quota: "events.quota",
1767
2222
  rate_limit: "events.rate_limit",
@@ -2100,6 +2555,25 @@ var dsh_connect_modelscope_token_plan_client = (function() {
2100
2555
  ]);
2101
2556
  const toggleDraw = useCallback((enabled) => void runDrawWrite(DRAW_PATH, { enabled }), [runDrawWrite]);
2102
2557
  const resetDraw = useCallback(() => void runDrawWrite(DRAW_RESET_PATH, {}), [runDrawWrite]);
2558
+ const saveDrawModel = useCallback((modelId) => void runDrawWrite(DRAW_PATH, { modelId }), [runDrawWrite]);
2559
+ const [trendBusy, setTrendBusy] = useState(false);
2560
+ const [trendError, setTrendError] = useState(null);
2561
+ const saveTrendWindow = useCallback(async (windowDays) => {
2562
+ setTrendBusy(true);
2563
+ setTrendError(null);
2564
+ try {
2565
+ await postWrite(TREND_WINDOW_PATH, { windowDays });
2566
+ load();
2567
+ } catch (reason) {
2568
+ setTrendError(writeErrorText(reason, tt));
2569
+ } finally {
2570
+ setTrendBusy(false);
2571
+ }
2572
+ }, [
2573
+ load,
2574
+ postWrite,
2575
+ tt
2576
+ ]);
2103
2577
  const [tokenBusy, setTokenBusy] = useState(false);
2104
2578
  const [tokenError, setTokenError] = useState(null);
2105
2579
  const saveToken = useCallback(async (value) => {
@@ -2181,7 +2655,13 @@ var dsh_connect_modelscope_token_plan_client = (function() {
2181
2655
  open: openSections.trend,
2182
2656
  onToggle: () => toggleSection("trend"),
2183
2657
  tt
2184
- }, h(TrendBars, {
2658
+ }, h(TrendWindowPicker, {
2659
+ trend: snap.trend,
2660
+ busy: trendBusy,
2661
+ error: trendError,
2662
+ onChange: (windowDays) => void saveTrendWindow(windowDays),
2663
+ tt
2664
+ }), h(TrendBars, {
2185
2665
  buckets: snap.trend?.buckets ?? [],
2186
2666
  tt
2187
2667
  })), h(SectionCard, {
@@ -2220,6 +2700,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
2220
2700
  error: drawError,
2221
2701
  onToggle: toggleDraw,
2222
2702
  onReset: resetDraw,
2703
+ onSelectModel: (id) => saveDrawModel(id),
2704
+ onAutoModel: () => saveDrawModel(null),
2223
2705
  tokenPresent: data?.token?.present === true,
2224
2706
  tt
2225
2707
  })));