dsh-connect-modelscope-token-plan 0.6.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -26,6 +26,47 @@
26
26
  > **发版时**:顶部开 `## [X.Y.Z] — YYYY-MM-DD`,完整流程见
27
27
  > [RELEASING.md](RELEASING.md) §3(本文开头这段是**写作者规范**,那是**发布操作流程**)。
28
28
 
29
+ ## [0.7.0] — 2026-10-26
30
+
31
+ **出图开关当场生效,不用再重启 DSH**
32
+
33
+ - 翻开「出图工具」开关,工具立刻可用;关掉立刻从工具列表消失。此前面板只提示「下次
34
+ 启动 DSH 才挂载」——挂上之后摘不掉,于是关掉开关工具仍会被调起。
35
+ - 卸载或热重载插件时工具随之下线,不在宿主的工具注册表里留下一个已失效却仍占名字的
36
+ 僵尸条目(旧行为下同名重挂会被宿主拒绝)。
37
+ - 旧版 Host 若不返回注销句柄,开关仍能挂上但摘不掉:面板直说「本机 Host 不支持卸载」,
38
+ 不再假装已经关掉。
39
+
40
+ **面板:「下次启动」的提示只剩失败场景**
41
+
42
+ - 这条提示改成瞬时读数:只在开关已开、几秒内还没挂上、且还查不出原因时出现;查得出
43
+ 原因的(缺 tools 服务 / 模块加载失败 / 注册被拒 / 不可摘)直接把原因写在卡上。
44
+
45
+ > 「宿主没有 unregister」这个断言当年是怎么写进注释、又怎么被推翻的(含 peer 契约
46
+ > 取证与判据):[docs/archive/2026-10-26-0.7.0-process.md](docs/archive/2026-10-26-0.7.0-process.md)
47
+
48
+ ## [0.6.6] — 2026-10-23
49
+
50
+ **对话框胶囊可以点开,还多了「· N 新模型」**
51
+
52
+ - 胶囊此前只是读数、点它没反应;现在点开是一张小卡:官方余额、今日本地调用、数据
53
+ 时间与口径说明都在里面,向上弹不遮草稿,Esc 或点外面收起。
54
+ - 目录里出现本机第一次见到的模型时,胶囊末尾多一段 `· 2 新模型`,面板里列出名字。
55
+ **口径是本机首见,不是网关新增**——目录不返回「何时上架」,只能记「我何时第一次
56
+ 看到它」。只报最近 7 天内首见的;首次运行只播种、不宣布,不会把整个目录报成新。
57
+
58
+ **诊断:多认一个状态文件**
59
+
60
+ - `npm run doctor` 现在也盘点 `roster-seen.json`(每个 profile 记了多少模型、其中几个
61
+ 是最近 7 天内首见)。文件损坏或版本不认识时与其他状态文件同款报出,不再只是「少了
62
+ 那句标记」而没有痕迹。
63
+
64
+ **模型窗口:两个新上架模型不再停在 128k 兜底**
65
+
66
+ - DeepSeek-V4.1-Flash-1017(视觉)与 Step-5-Preview 上架时不在内置窗口策展表里,
67
+ 面板只能显示 128k 兜底,而它们在 models.dev 各厂 host 下的共识是 1M。现在按上游
68
+ 共识显示 1M。
69
+
29
70
  ## [0.6.5] — 2026-10-21
30
71
 
31
72
  **出图:被拒的图片地址不再原样回显**
package/README.md CHANGED
@@ -1,4 +1,22 @@
1
- # dsh-connect-modelscope-token-plan
1
+ <h1 align="center">dsh-connect-modelscope-token-plan</h1>
2
+
3
+ <p align="center"><b>把魔搭社区(ModelScope)API-Inference 免费额度的面板接进 DSH,并可选把魔搭模型注册成 LLM provider。</b></p>
4
+
5
+ <p align="center">
6
+ <a href="#安装">安装</a> ·
7
+ <a href="#面板长什么样">工作原理</a> ·
8
+ <a href="CHANGELOG.md">更新日志</a> ·
9
+ <a href="https://github.com/eghrhegpe/dsh-connect-modelscope-token-plan/issues">问题反馈</a>
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://www.npmjs.com/package/dsh-connect-modelscope-token-plan"><img src="https://img.shields.io/npm/v/dsh-connect-modelscope-token-plan?style=flat-square&label=npm&color=cb3837" alt="npm version"></a>
14
+ <a href="https://www.npmjs.com/package/dsh-connect-modelscope-token-plan"><img src="https://img.shields.io/npm/d18m/dsh-connect-modelscope-token-plan?style=flat-square&label=downloads&color=cb3837" alt="npm downloads"></a>
15
+ <a href="https://github.com/eghrhegpe/dsh-connect-modelscope-token-plan/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/eghrhegpe/dsh-connect-modelscope-token-plan/ci.yml?branch=main&style=flat-square&label=tests" alt="test status"></a>
16
+ <a href="LICENSE"><img src="https://img.shields.io/github/license/eghrhegpe/dsh-connect-modelscope-token-plan?style=flat-square" alt="MIT license"></a>
17
+ <a href="https://github.com/eghrhegpe/dsh-connect-modelscope-token-plan/stargazers"><img src="https://img.shields.io/github/stars/eghrhegpe/dsh-connect-modelscope-token-plan?style=flat-square" alt="GitHub stars"></a>
18
+ <a href="https://dshfind.com/plugins/eghrhegpe/dsh-connect-modelscope-token-plan"><img src="https://dshfind.com/api/badge/eghrhegpe/dsh-connect-modelscope-token-plan" alt="dshfind plugin"></a>
19
+ </p>
2
20
 
3
21
  把魔搭社区(modelscope.cn)**API-Inference 免费额度**的面板接进 DeepSeek Harness 的 **Plugins 页**(插件卡内联,三个 tab:额度 / 模型 / 接入),并可选把魔搭模型注册成 **DSH 的 LLM provider**(id `modelscope-token-plan`,OpenAI 兼容)——注册后魔搭模型进 DSH 模型选择器,可直接对话调用;还能开**出图工具**做文生图 / 图生图。
4
22
 
@@ -25,7 +43,7 @@
25
43
 
26
44
  ![「模型」tab:接入为DSH 模型与模型清单](assets/tab-model-list.png)
27
45
 
28
- - 同一 tab 下方有**「出图工具」**卡:开启后支持文生图、图生图与多图编辑。它与「接入为 DSH 模型」是两条独立开关,开关值立即保存、**工具的挂载发生在下次 Host 启动**。出图是异步的:一次没出好会返回 `status: pending` 与 `jobId`,带上它再调一次即可续查(不必重出)。协议细节见 [docs/SPIKE-image-gen.md](docs/SPIKE-image-gen.md)(2026-10-10 实测,v0.2 起已实现)。
46
+ - 同一 tab 下方有**「出图工具」**卡:开启后支持文生图、图生图与多图编辑。它与「接入为 DSH 模型」是两条独立开关,开关保存后**当场**挂/卸工具(不用重启)。出图是异步的:一次没出好会返回 `status: pending` 与 `jobId`,带上它再调一次即可续查(不必重出)。协议细节见 [docs/SPIKE-image-gen.md](docs/SPIKE-image-gen.md)(2026-10-10 实测,v0.2 起已实现)。
29
47
  - 卡片底部有**「上下文窗口覆盖」**选择器(不覆盖 / 128K / 256K / 512K / 1M):魔搭目录不返回窗口字段,插件按「目录声明 → 你选的档位 → 内置策展表(models.dev 交叉真值)→ 128K 兜底」四级兜底;覆盖压过策展表。档位来源与「为什么不给输出上限内置策展表」见 [docs/PROVIDER-M4.md](docs/PROVIDER-M4.md)。
30
48
 
31
49
  **「接入」tab**——粘贴一枚 `ms-…` 访问令牌即完成凭据配置(令牌永不入日志、永不进插件目录)。没有令牌时面板会提示你到这里来填。
@@ -46,6 +64,8 @@
46
64
  - **只在当前选中的 provider 是 `modelscope-token-plan` 时出现**,切到别的 provider 整块消失、不发任何请求。
47
65
  - **没有百分比、没有黄色档**:魔粒是存量、官方没有发布上限或低额阈值,造进度条 / 阈值都是给沒人公布过的分母配数。三档只有绿(正)/ 红(官方确为 0)/ 灰(读不到)。
48
66
  - **读不到 ≠ 0**:读不到时该段渲染「—」,0 只在官方真的给 0 时出现并走告警色;读失败保留上次数值并标「上次数据」。
67
+ - **点开是详情面板**:胶囊可点,向上弹一张小卡——官方余额、今日本地调用、数据时间与口径说明都在里面,不遮草稿。
68
+ - **「· N 新模型」**(v0.6.6 起):目录里出现本机**第一次见到**的模型时,胶囊末尾多一段 `· 2 新模型`,详情面板里列出名字。**口径是本机首见,不是网关新增**——魔搭目录不返回「何时上架」,只能记「我什么时候第一次看到它」,两者混称就是编数据。只报最近 7 天内首见的,且只记目录 id;促销 / 活动变化不在射程内(那些是页面上的实时信息,不是目录事实)。首次运行只播种、不宣布:装上这个版本后的第一轮刷新不会把整个目录报成「新」。
49
69
 
50
70
  ## 安装
51
71
 
@@ -60,7 +80,7 @@
60
80
 
61
81
  3. 打开 DSH 的 **Plugins 页**,找到本插件的插件卡(面板是页内联卡片,**不在侧边栏**)。
62
82
 
63
- > 装到 npm 的那一份**不含 `docs/`**(`package.json` 的 `files` 白名单只带 `lib/` / `client.js` / `assets/` / `README.md` / `CHANGELOG.md` / `LICENSE` 等),下文指向 `docs/*` 的链接在已安装副本上打不开,请到 GitHub 仓库看。
83
+ > **分发面**:npm 包只含 `package.json#files` 那 11 项(`lib/`、`client.js`、`icon.svg`、`locale/`、`assets/`、`cordis.patch.yml`、`README.md`、`CHANGELOG.md`、`LICENSE`、`THIRD_PARTY_NOTICES.md`、`screenshots.json`)。`docs/`、`AGENTS.md`、`RELEASING.md`、`tools/`、`test/` **不在包里**——下文指向它们的链接(以及「文档」一节的两个开发者入口)在 registry 页面上会 404,请到 GitHub 仓库看;截图因 `assets/` 在包内,相对路径在两边都能显示。
64
84
 
65
85
  ## 免费额度怎么算(2026-10 快照,以官方为准)
66
86
 
@@ -96,16 +116,10 @@ npm run doctor:json # 机器读:JSON(可进你的巡检 / 工单脚本
96
116
 
97
117
  ## 文档
98
118
 
99
- - [docs/SPIKE.md](docs/SPIKE.md) — 上游到底有没有额度信号(实测记录 + 结论)
100
- - [docs/SPIKE-image-gen.md](docs/SPIKE-image-gen.md) — 出图协议实测(2026-10-10;v0.2 起已实现)
101
- - [docs/PROVIDER-M4.md](docs/PROVIDER-M4.md) — provider 接入实现契约(设计裁定都在这里)
102
- - [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md) — 这版怎么做出来的、踩过什么坑
103
- - [docs/REFERENCES.md](docs/REFERENCES.md)、[docs/REFERENCE-modelsdev.md](docs/REFERENCE-modelsdev.md) — 上游端点全景 / 第三方方案对比
104
- - [docs/ROADMAP.md](docs/ROADMAP.md) — v1 要做什么、做到哪了
105
- - [docs/archive/](docs/archive/) — 已冻结的逐版修复档案
119
+ **文档地图只有一份:[`AGENTS.md` §去哪查](https://github.com/eghrhegpe/dsh-connect-modelscope-token-plan/blob/master/AGENTS.md)**(每份文档一行「何时查」)。这里不再列第二遍——两处索引必然分叉,而分叉之后没人知道哪份是对的。
106
120
 
107
- AI 协作会话请先读 [AGENTS.md](AGENTS.md)(验证入口、红线与文档地图);开发者校验工具(变异测试 / 覆盖率)见 [tools/dev/README.md](tools/dev/README.md);发布流程见 [RELEASING.md](RELEASING.md)。
121
+ 开发者校验工具(变异测试 / 覆盖率)见 [`tools/dev/README.md`](https://github.com/eghrhegpe/dsh-connect-modelscope-token-plan/blob/master/tools/dev/README.md),发布流程见 [`RELEASING.md`](https://github.com/eghrhegpe/dsh-connect-modelscope-token-plan/blob/master/RELEASING.md)。配置字段的人读清单见 [docs/CONFIG.md](docs/CONFIG.md)(`drawEnabled` 这类「只有 YAML 注释」的键都在那里)。
108
122
 
109
123
  ## 许可证
110
124
 
111
- MIT License,全文见 [LICENSE](LICENSE)。
125
+ MIT License,全文见 [LICENSE](LICENSE);第三方内容(`docs/modelscope.cn-openapi/` 的魔搭 OpenAPI 转储等)见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。
@@ -0,0 +1,32 @@
1
+ # 第三方致谢与合规(Third-Party Notices)
2
+
3
+ 本插件的源码与文档均为本仓库原创,按根目录 [LICENSE](./LICENSE)(MIT)授权。下列是**唯一的**
4
+ 第三方内容:
5
+
6
+ ## 1. `docs/modelscope.cn-openapi/` —— 魔搭站点 OpenAPI 规格转储
7
+
8
+ | 项 | 内容 |
9
+ |---|---|
10
+ | 出处 | 魔搭社区站点 `https://modelscope.cn/openapi/v1`(机读规格),2026-10-10 单提交入库(`6b9f1c0`),此后未更新 |
11
+ | 内容 | `modelscope.cn-openapi.md`(可读全文)与 `modelscope.cn-openapi.json.gz`(原始规格);目录内的 `README.md` 由本仓编写,说明转储口径 |
12
+ | 用途 | **契约参照**:核对面板外链/接口字段时查,不是数据源,也不参与任何请求 |
13
+ | 代码是否消费 | **否**——`test/docs.test.mjs` 的 `isVendoredSpec` 按目录前缀把整个目录排除在文档断言之外 |
14
+ | 权利 | 文档内容版权归魔搭社区 / 阿里巴巴所有,此处按「引用与核对」用途收录,未做商业再分发 |
15
+ | 是否随 npm 包发布 | **否**。`package.json#files` 不含 `docs/` |
16
+
17
+ ## 2. 与本插件通信的上游服务
18
+
19
+ - **魔搭 API-Inference**(`api-inference.modelscope.cn`)与站点(`modelscope.cn`):插件以使用者
20
+ 自己的访问令牌调用其模型目录、对话接口与官方余额接口。这些**不是**本仓库的代码,也不随包分发;
21
+ 使用须遵守魔搭的服务条款。
22
+ - **DSH(DeepSeek Harness)**:宿主运行时提供 `webServer` / `settings` / `llm` / `credentials`
23
+ 等 peer 服务,由宿主安装,不随本包分发。
24
+
25
+ ## 3. 开发期工具链
26
+
27
+ 构建与测试使用的开源包(如 `tsdown`、`typescript`、`react` 作为 peer)按其各自许可证使用,
28
+ **不复制其源码进本仓库**;依赖清单见 `package.json`,锁文件入库。
29
+
30
+ ## 4. 与厂商的关系
31
+
32
+ 本项目为个人集成作品,与魔搭社区、阿里巴巴、DeepSeek 均无关联,未获其背书。
package/client.js CHANGED
@@ -223,7 +223,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
223
223
  "draw.busy": "切换中…",
224
224
  "draw.mounted": "已注册:{tool}",
225
225
  "draw.notMounted": "未注册——打开上方开关即可开启。",
226
- "draw.restartHint": "开关立即保存;工具要到下次启动 DSH 才挂载(挂载后无法撤销)。",
226
+ "draw.restartHint": "开关立即生效;工具正在挂载,若几秒后仍未挂上,上方会给出原因。",
227
227
  "draw.currentNone": "当前目录里没有可出图的模型——出图会失败(先确认访问令牌已配置、目录已拉到)。",
228
228
  "draw.currentSrc.declaredTextToImage": "平台声明为文生图",
229
229
  "draw.currentSrc.knownTextToImage": "已知可纯文生图",
@@ -245,6 +245,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
245
245
  "draw.absent.noTools": "开关已开,但本机没有 tools 服务,工具没挂上(Host 侧限制,不是配置问题)。",
246
246
  "draw.absent.peerLoad": "开关已开,但 tools 模块加载失败,出图工具缺席(Host 问题,非配置)。",
247
247
  "draw.absent.registry": "开关已开,但工具注册被 Host 拒绝,出图工具缺席(Host 问题,非配置)。",
248
+ "draw.absent.notRemovable": "工具已注册,但本机 Host 不返回注销句柄——关掉开关也摘不掉它(旧 Host 限制)。",
248
249
  "draw.absent.unknown": "开关已开,但出图工具未挂载,原因未知。",
249
250
  "draw.preferredGone": "已保存的默认模型 {model} 已不在当前目录(下架或改名)——出图会用它失败,请在下面重选。",
250
251
  "trend.none": "还没有本地调用记录。",
@@ -286,7 +287,16 @@ var dsh_connect_modelscope_token_plan_client = (function() {
286
287
  "composer.pillTitle": "魔搭额度:官方魔粒余额 + 今日本地调用次数",
287
288
  "composer.balanceUnit": "魔粒",
288
289
  "composer.localCalls": "今日本地 {calls} 次",
289
- "composer.stale": "上次数据"
290
+ "composer.stale": "上次数据",
291
+ "composer.panelTitle": "魔搭额度详情",
292
+ "composer.panelBalance": "官方魔粒余额",
293
+ "composer.panelCalls": "今日本地调用",
294
+ "composer.panelNote": "余额来自官方接口,调用次数由本机按本地日统计——两者口径不同,不互相换算。",
295
+ "composer.newModels": "新模型",
296
+ "composer.newModelsRow": "本机首次见到的模型",
297
+ "composer.newModelsTitle": "本机首次见到的模型:",
298
+ "composer.newModelsSep": "、",
299
+ "composer.newModelsNote": "「新」指本机首次记到它,与上游何时上架无关。只列最近 7 天内首见的。"
290
300
  };
291
301
  en = {
292
302
  "panel.refresh": "Refresh",
@@ -377,7 +387,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
377
387
  "draw.busy": "Switching…",
378
388
  "draw.mounted": "Registered: {tool}",
379
389
  "draw.notMounted": "Not registered — turn the switch above on.",
380
- "draw.restartHint": "The switch saves at once; the tool mounts the next time DSH starts (a mount cannot be undone).",
390
+ "draw.restartHint": "The switch takes effect immediately; the tool is still mounting. If it stays unmounted, the reason appears above.",
381
391
  "draw.currentNone": "No image model in the catalogue right now — drawing will fail (check the access token and that the catalogue has loaded).",
382
392
  "draw.currentSrc.declaredTextToImage": "platform-declared text-to-image",
383
393
  "draw.currentSrc.knownTextToImage": "known to accept text-to-image",
@@ -399,6 +409,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
399
409
  "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).",
400
410
  "draw.absent.peerLoad": "The switch is on, but the tools module failed to load; the draw tool is absent (a Host fault, not config).",
401
411
  "draw.absent.registry": "The switch is on, but the Host refused the tool registration; the draw tool is absent (a Host fault, not config).",
412
+ "draw.absent.notRemovable": "The tool is registered, but this Host returns no disposer — turning the switch off cannot unmount it (an older Host limitation).",
402
413
  "draw.absent.unknown": "The switch is on, but the draw tool is not mounted; the reason is unknown.",
403
414
  "draw.preferredGone": "The saved default model {model} is no longer in the catalogue (delisted or renamed) — drawing with it will fail. Pick another one below.",
404
415
  "trend.none": "No local call records yet.",
@@ -440,7 +451,16 @@ var dsh_connect_modelscope_token_plan_client = (function() {
440
451
  "composer.pillTitle": "ModelScope quota: official Magicube balance + today's local call count",
441
452
  "composer.balanceUnit": "Magicubes",
442
453
  "composer.localCalls": "local today {calls}",
443
- "composer.stale": "stale data"
454
+ "composer.stale": "stale data",
455
+ "composer.panelTitle": "ModelScope quota details",
456
+ "composer.panelBalance": "Official Magicube balance",
457
+ "composer.panelCalls": "Calls today (local)",
458
+ "composer.panelNote": "The balance comes from the official endpoint; the call count is tallied on this device by local day — different sources, so they are not converted into each other.",
459
+ "composer.newModels": "new",
460
+ "composer.newModelsRow": "First seen on this device",
461
+ "composer.newModelsTitle": "First seen on this device: ",
462
+ "composer.newModelsSep": ", ",
463
+ "composer.newModelsNote": "\"New\" means first recorded on this device, not newly listed by the gateway. Only models first seen within the last 7 days are listed."
444
464
  };
445
465
  }));
446
466
 
@@ -998,7 +1018,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
998
1018
  fontSize: 12,
999
1019
  fontWeight: 600,
1000
1020
  lineHeight: "18px",
1001
- cursor: "default",
1021
+ cursor: "pointer",
1002
1022
  flexShrink: 0,
1003
1023
  whiteSpace: "nowrap"
1004
1024
  },
@@ -1017,7 +1037,69 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1017
1037
  pillTierOk: "#22c55e",
1018
1038
  pillTierExhausted: "#ef4444",
1019
1039
  pillTierUnknown: "#9ca3af",
1020
- pillTierExhaustedText: { color: "var(--dsw-alias-state-error-primary)" }
1040
+ pillTierExhaustedText: { color: "var(--dsw-alias-state-error-primary)" },
1041
+ pillNewTag: {
1042
+ color: "var(--dsw-alias-label-secondary)",
1043
+ flexShrink: 0
1044
+ },
1045
+ pillPopoverAnchor: {
1046
+ position: "relative",
1047
+ display: "inline-flex"
1048
+ },
1049
+ pillPopover: {
1050
+ position: "absolute",
1051
+ bottom: "100%",
1052
+ right: 0,
1053
+ zIndex: 100,
1054
+ border: "1px solid var(--dsw-alias-border-l1)",
1055
+ borderRadius: 10,
1056
+ background: "var(--dsw-alias-bg-layer-1)",
1057
+ boxShadow: "0 6px 24px rgba(0, 0, 0, 0.18)",
1058
+ width: "min(320px, calc(100vw - 32px))",
1059
+ padding: "10px 12px",
1060
+ marginBottom: 8,
1061
+ textAlign: "left",
1062
+ color: "var(--dsw-alias-label-primary)",
1063
+ fontSize: 12,
1064
+ lineHeight: "18px",
1065
+ fontFamily: "inherit",
1066
+ boxSizing: "border-box"
1067
+ },
1068
+ pillPopoverHead: {
1069
+ display: "flex",
1070
+ alignItems: "center",
1071
+ gap: 6,
1072
+ flexWrap: "wrap",
1073
+ marginBottom: 8
1074
+ },
1075
+ pillPopoverTitle: {
1076
+ fontSize: 12,
1077
+ fontWeight: 600,
1078
+ color: "var(--dsw-alias-label-primary)"
1079
+ },
1080
+ pillPopoverStale: {
1081
+ color: "var(--dsw-alias-label-tertiary)",
1082
+ fontStyle: "italic"
1083
+ },
1084
+ pillPopoverRow: { padding: "6px 0" },
1085
+ pillPopoverRowLine: {
1086
+ display: "flex",
1087
+ alignItems: "baseline",
1088
+ gap: 8
1089
+ },
1090
+ pillPopoverLabel: {
1091
+ fontSize: 12,
1092
+ color: "var(--dsw-alias-label-secondary)"
1093
+ },
1094
+ pillPopoverFigure: {
1095
+ marginLeft: "auto",
1096
+ fontVariantNumeric: "tabular-nums",
1097
+ color: "var(--dsw-alias-label-primary)"
1098
+ },
1099
+ pillPopoverNote: {
1100
+ marginTop: 8,
1101
+ color: "var(--dsw-alias-label-tertiary)"
1102
+ }
1021
1103
  };
1022
1104
  }));
1023
1105
 
@@ -1193,6 +1275,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1193
1275
  allowed: p.allowed === "none" ? "none" : p.allowed === "list" ? "list" : "all",
1194
1276
  enabledIds: ids,
1195
1277
  contextWindow: typeof p.contextWindow === "number" && Number.isFinite(p.contextWindow) && p.contextWindow > 0 ? p.contextWindow : null,
1278
+ newModelIds: Array.isArray(p.newModelIds) ? p.newModelIds.map((id) => String(id ?? "")).filter((id) => id !== "") : [],
1196
1279
  roster
1197
1280
  };
1198
1281
  }
@@ -1207,8 +1290,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1207
1290
  /**
1208
1291
  * 把 wire/路由返回的出图块归一成 `DrawStatus`;不合法一律降级。
1209
1292
  *
1210
- * 与 `providerOf` 同一套防御性归一(旧 Host 可能缺字段)。`absentReason` 只认三个
1211
- * 已知码——上游若新增第四个,归一到 `null` 而不是把它透传成渲染不出的值;面板的
1293
+ * 与 `providerOf` 同一套防御性归一(旧 Host 可能缺字段)。`absentReason` 只认四个
1294
+ * 已知码——上游若新增第五个,归一到 `null` 而不是把它透传成渲染不出的值;面板的
1212
1295
  * 「缺席」措辞有兜底键,但**状态本身**宁可显示「未知」也不假装认识它。
1213
1296
  * @param {unknown} raw - 原始块。
1214
1297
  * @returns {DrawStatus} 归一后的状态。
@@ -1218,7 +1301,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1218
1301
  const d = raw;
1219
1302
  const candidates = Array.isArray(d.candidateIds) ? d.candidateIds.map((id) => String(id ?? "")).filter((id) => id !== "") : [];
1220
1303
  const reason = d.absentReason;
1221
- const absentReason = reason === "no-tools-service" || reason === "peer-load-failed" || reason === "registry-refused" ? reason : null;
1304
+ const absentReason = reason === "no-tools-service" || reason === "peer-load-failed" || reason === "registry-refused" || reason === "not-removable" ? reason : null;
1222
1305
  const rawCandidates = Array.isArray(d.candidates) ? d.candidates.map((entry) => {
1223
1306
  const row = entry ?? {};
1224
1307
  return {
@@ -1286,6 +1369,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1286
1369
  allowed: "all",
1287
1370
  enabledIds: [],
1288
1371
  contextWindow: null,
1372
+ newModelIds: [],
1289
1373
  roster: []
1290
1374
  });
1291
1375
  DEGRADED_DRAW = Object.freeze({
@@ -2128,15 +2212,15 @@ var dsh_connect_modelscope_token_plan_client = (function() {
2128
2212
  /**
2129
2213
  * 出图工具卡:开关、候选模型、挂载状态。
2130
2214
  *
2131
- * **与 ProviderCard 的关键差异,也是这张卡存在的主要理由**:provider 开关翻转会
2132
- * 立即重注册,而出图工具的开关翻转**要等下次 Host 启动**——peer 的 tools registry
2133
- * 没有 unregister(完整论述见 src/host/draw-register.ts 文件头注)。所以这张卡必须
2134
- * 把这件事说出来(`draw.restartHint`),否则用户
2135
- * 翻开关后看不到任何变化,会以为坏了。
2215
+ * **与 ProviderCard 的关键差异**:provider 开关翻转是「重建 provider 对」,出图工具
2216
+ * 的开关翻转是「注册/注销一个 agent 工具」——两者都在**本次 Host 生命周期内**生效
2217
+ * (peer 的 `tools.register` 返回 disposer,完整论述见 src/host/draw-register.ts 文件
2218
+ * 头注)。保存后路由会等挂载收敛完再回快照,所以这张卡通常直接显示「已注册」;
2219
+ * `draw.restartHint` 只在瞬时未挂上(或挂载失败)时出现,不再宣称「要等下次启动」。
2136
2220
  *
2137
- * 缺席原因是**分开渲染**的:`no-tools-service` 是「这个 Host 没提供该服务」(正常),
2138
- * 另两种是 Host 缺陷。三种都给出各自的一句话,因为「开关开着但工具不在」如果没有
2139
- * 解释,用户没有任何办法自己诊断。
2221
+ * 缺席/不可摘原因是**分开渲染**的:`no-tools-service` 是「这个 Host 没提供该服务」
2222
+ * (正常),`not-removable` 是「能用但关不掉」,另两种是 Host 缺陷。每种都给出各自
2223
+ * 的一句话,因为「开关开着但工具不在」如果没有解释,用户没有任何办法自己诊断。
2140
2224
  * @param {object} props - 渲染参数。
2141
2225
  * @returns {unknown} 卡片节点。
2142
2226
  */
@@ -2191,7 +2275,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
2191
2275
  },
2192
2276
  role: "status"
2193
2277
  }, tt("draw.notMounted"));
2194
- const restartHint = enabled && status.mountPending === true ? h("div", { style: {
2278
+ const restartHint = enabled && status.mountPending === true && status.absentReason === null ? h("div", { style: {
2195
2279
  ...S.formNote,
2196
2280
  color: "var(--dsw-alias-state-warn-primary)"
2197
2281
  } }, tt("draw.restartHint")) : null;
@@ -2349,7 +2433,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
2349
2433
  DRAW_ABSENT_KEY = {
2350
2434
  "no-tools-service": "draw.absent.noTools",
2351
2435
  "peer-load-failed": "draw.absent.peerLoad",
2352
- "registry-refused": "draw.absent.registry"
2436
+ "registry-refused": "draw.absent.registry",
2437
+ "not-removable": "draw.absent.notRemovable"
2353
2438
  };
2354
2439
  }));
2355
2440
 
@@ -3013,15 +3098,109 @@ var dsh_connect_modelscope_token_plan_client = (function() {
3013
3098
  return parts.filter((part) => part !== null).join(" · ");
3014
3099
  }
3015
3100
  /**
3101
+ * 「本机首次见到的模型」清单:`snapshot.provider.newModelIds` 的净味版。
3102
+ *
3103
+ * 三道过滤都是承重的:
3104
+ * - 非数组(老 Host / 降级快照)→ 空清单,不抛;
3105
+ * - 非字符串 / 空串 → 丢掉,否则面板会画出一个空名字的「新模型」;
3106
+ * - 去重 → Host 侧已经去过重,但客户端不能依赖对端,重复项会让「3 个新模型」
3107
+ * 变成 4 行同名。
3108
+ */
3109
+ function newModelsOf(snapshot) {
3110
+ const raw = snapshot?.provider?.newModelIds;
3111
+ if (!Array.isArray(raw)) return [];
3112
+ const seen = /* @__PURE__ */ new Set();
3113
+ const ids = [];
3114
+ for (const value of raw) {
3115
+ if (typeof value !== "string") continue;
3116
+ const id = value.trim();
3117
+ if (id === "" || seen.has(id)) continue;
3118
+ seen.add(id);
3119
+ ids.push(id);
3120
+ }
3121
+ return ids;
3122
+ }
3123
+ /**
3124
+ * 点面板外面或按 Escape 关面板。
3125
+ *
3126
+ * 三处细节各有理由:
3127
+ * - `pointerdown` 而不是 `click`:`click` 触发时按钮自己的 onClick 已经跑过,
3128
+ * 一个把触发器算作「里面」的 click 监听会先关再被 toggle 打开,面板永远关不掉;
3129
+ * - 包含判定读**锚节点**而不是面板:点触发器本身是一次 toggle,把它算作「里面」
3130
+ * 才不会和 toggle 抢;
3131
+ * - 鸭子类型判 `nodeType` 而不是 `instanceof Node`:本模块在 jsdom 里也跑,
3132
+ * 而 jsdom 只把 `document` 镜像进 Node realm,`Node` 构造函数在那儿不存在。
3133
+ *
3134
+ * `open === false` 时不挂监听(没有面板要关,挂上就是白跑)。
3135
+ */
3136
+ function useDismissOn(open, close, anchorRef) {
3137
+ useEffect(() => {
3138
+ if (!open) return;
3139
+ if (typeof document === "undefined" || !("addEventListener" in document)) return;
3140
+ const nodeLike = (value) => typeof value === "object" && value !== null && typeof value.nodeType === "number";
3141
+ const anchorLike = anchorRef.current;
3142
+ const insideAnchor = (target) => nodeLike(target) && typeof anchorLike?.contains === "function" && anchorLike.contains(target);
3143
+ const onPointerDown = (event) => {
3144
+ if (insideAnchor(event.target)) return;
3145
+ close();
3146
+ };
3147
+ const onKeyDown = (event) => {
3148
+ if (event.key !== "Escape") return;
3149
+ close();
3150
+ };
3151
+ document.addEventListener("pointerdown", onPointerDown, true);
3152
+ document.addEventListener("keydown", onKeyDown);
3153
+ return () => {
3154
+ document.removeEventListener("pointerdown", onPointerDown, true);
3155
+ document.removeEventListener("keydown", onKeyDown);
3156
+ };
3157
+ }, [
3158
+ open,
3159
+ close,
3160
+ anchorRef
3161
+ ]);
3162
+ }
3163
+ /**
3164
+ * 展开面板:把胶囊正文那两段摊成两行,再补胶囊装不下的东西(口径注脚、
3165
+ * 读取时间、新模型清单)。
3166
+ *
3167
+ * 面板是触发器的**兄弟**而不是子节点:`<button>` 里塞 `<div role="dialog">`
3168
+ * 是非法嵌套(按钮的内容模型是 phrasing content,交互内容更糟)。锚壳用
3169
+ * `<span>` 同理。
3170
+ */
3171
+ function QuotaPopover(props) {
3172
+ const { tt, title, stale, available, usedToday, balance, newModels } = props;
3173
+ const fetchedAt = balance?.fetchedAt;
3174
+ const failure = balance?.error;
3175
+ return h("div", {
3176
+ className: "dsh-modelscope-composer-pill-popover",
3177
+ id: POPOVER_ID,
3178
+ role: "dialog",
3179
+ "aria-label": title,
3180
+ style: S.pillPopover
3181
+ }, h("div", { style: S.pillPopoverHead }, h("span", { style: S.pillPopoverTitle }, tt("composer.panelTitle")), stale ? h("span", { style: S.pillPopoverStale }, tt("composer.stale")) : null), h("div", { style: S.pillPopoverRow }, h("div", { style: S.pillPopoverRowLine }, h("span", { style: S.pillPopoverLabel }, tt("composer.panelBalance")), h("span", { style: S.pillPopoverFigure }, balanceText(available, tt)))), h("div", { style: S.pillPopoverRow }, h("div", { style: S.pillPopoverRowLine }, h("span", { style: S.pillPopoverLabel }, tt("composer.panelCalls")), h("span", { style: S.pillPopoverFigure }, usedToday === null ? "—" : format(tt("composer.localCalls"), { calls: count(usedToday) })))), newModels.length === 0 ? null : h("div", { style: S.pillPopoverRow }, h("div", { style: S.pillPopoverRowLine }, h("span", { style: S.pillPopoverLabel }, tt("composer.newModelsRow")), h("span", { style: S.pillPopoverFigure }, newModels.join(tt("composer.newModelsSep"))))), failure !== void 0 && typeof failure === "string" && failure !== "" ? h("div", { style: S.pillPopoverNote }, format(tt("balance.unavailable"), { error: failure })) : null, fetchedAt !== void 0 && typeof fetchedAt === "string" && fetchedAt !== "" ? h("div", { style: S.pillPopoverNote }, format(tt("balance.fetched"), { time: isoTime(fetchedAt) })) : null, h("div", { style: S.pillPopoverNote }, tt("composer.panelNote")), newModels.length === 0 ? null : h("div", { style: S.pillPopoverNote }, tt("composer.newModelsNote")));
3182
+ }
3183
+ /**
3016
3184
  * 内层:只在会话选中本插件的模型时挂载。轮询 hook 在这里,被挂/摘即起停——
3017
3185
  * 非归属 provider 的会话不发任何请求。
3186
+ *
3187
+ * 展开状态也住在这里(而不是外层门控):外层每帧都要调 `useProjection` 才能
3188
+ * 决定挂谁,把 `useState` 加到外层会让「非归属 → 归属」这个切换改变 hook 顺序
3189
+ * (外层先 return null 再挂内层,Rules of Hooks 会炸)。内层被挂/摘时它自己的
3190
+ * hook 序是完整的,所以 open 放这里安全。
3018
3191
  */
3019
3192
  function ModelscopePill(props) {
3020
3193
  const { tt } = props;
3021
3194
  const { data, error, loadedOnce, updatedAt } = useSnapshotPolling();
3195
+ const [open, setOpen] = useState(false);
3196
+ const anchorRef = useRef(null);
3197
+ const close = useCallback(() => setOpen(false), []);
3198
+ const toggle = useCallback(() => setOpen((current) => !current), []);
3199
+ useDismissOn(open, close, anchorRef);
3022
3200
  const balance = data?.balance ?? null;
3023
3201
  const available = availableOf(balance);
3024
3202
  const usedToday = usedTodayOf(data);
3203
+ const newModels = newModelsOf(data);
3025
3204
  const cadenceMs = statedCadenceMs(data?.pollSeconds, DEFAULT_CADENCE_MS);
3026
3205
  const stale = staleOf({
3027
3206
  error,
@@ -3038,11 +3217,15 @@ var dsh_connect_modelscope_token_plan_client = (function() {
3038
3217
  usedToday,
3039
3218
  stale
3040
3219
  });
3041
- return h("button", {
3220
+ const trigger = h("button", {
3042
3221
  type: "button",
3043
3222
  className: "dsh-modelscope-composer-pill",
3044
3223
  title,
3045
3224
  "aria-label": title,
3225
+ "aria-expanded": open,
3226
+ "aria-haspopup": "dialog",
3227
+ "aria-controls": open ? POPOVER_ID : void 0,
3228
+ onClick: toggle,
3046
3229
  style: S.pillTrigger
3047
3230
  }, h("span", {
3048
3231
  className: "dsh-modelscope-composer-pill-dot",
@@ -3060,7 +3243,24 @@ var dsh_connect_modelscope_token_plan_client = (function() {
3060
3243
  }, pending ? "…" : readingText(available, usedToday, tt)), stale ? h("span", {
3061
3244
  className: "dsh-modelscope-composer-pill-stale",
3062
3245
  style: S.pillStale
3063
- }, ` · ${tt("composer.stale")}`) : null);
3246
+ }, ` · ${tt("composer.stale")}`) : null, newModels.length === 0 ? null : h("span", {
3247
+ className: "dsh-modelscope-composer-pill-new",
3248
+ style: S.pillNewTag,
3249
+ title: `${tt("composer.newModelsTitle")}${newModels.slice(0, 3).join(tt("composer.newModelsSep"))}${newModels.length > 3 ? "…" : ""}`
3250
+ }, ` · ${newModels.length} ${tt("composer.newModels")}`));
3251
+ return h("span", {
3252
+ className: "dsh-modelscope-composer-pill-anchor",
3253
+ style: S.pillPopoverAnchor,
3254
+ ref: anchorRef
3255
+ }, trigger, open ? h(QuotaPopover, {
3256
+ tt,
3257
+ title,
3258
+ stale,
3259
+ available,
3260
+ usedToday,
3261
+ balance,
3262
+ newModels
3263
+ }) : null);
3064
3264
  }
3065
3265
  /**
3066
3266
  * 外层门控:固定一处调 `useProjection`,再决定内层挂谁。非归属 provider 返回
@@ -3071,7 +3271,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
3071
3271
  if (selectedProviderOf(selection) !== "modelscope-token-plan") return null;
3072
3272
  return h(ModelscopePill, { tt: props.tt });
3073
3273
  }
3074
- var MODELSCOPE_PROVIDER, DEFAULT_CADENCE_MS, STALE_CADENCES, TIER_INK;
3274
+ var MODELSCOPE_PROVIDER, DEFAULT_CADENCE_MS, STALE_CADENCES, TIER_INK, POPOVER_ID;
3075
3275
  var init_composer_usage_pill = __esmMin((() => {
3076
3276
  init_runtime();
3077
3277
  init_styles();
@@ -3085,6 +3285,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
3085
3285
  exhausted: S.pillTierExhausted,
3086
3286
  unknown: S.pillTierUnknown
3087
3287
  };
3288
+ POPOVER_ID = "dsh-modelscope-composer-pill-popover";
3088
3289
  }));
3089
3290
 
3090
3291
  //#endregion
@@ -3208,7 +3409,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
3208
3409
  balanceText,
3209
3410
  callsText,
3210
3411
  readingText,
3211
- staleOf
3412
+ staleOf,
3413
+ newModelsOf
3212
3414
  }),
3213
3415
  components: Object.freeze({
3214
3416
  BalanceCard,
package/cordis.patch.yml CHANGED
@@ -102,10 +102,9 @@
102
102
  # · 出图**不扣**魔粒(chat 才扣,1 次 = 1 魔粒),所以面板刻意**不显示**
103
103
  # 出图计数——那会变成拿 chat 的口径给生图记账。
104
104
  #
105
- # 生效时机:开关值立即保存,但**工具的挂载发生在下次 Host 启动**——
106
- # peer 的 tools registry 没有 unregister,工具一旦注册就摘不下来(机制与
107
- # 后果的完整论述见 src/host/draw-register.ts 文件头注)。
108
- # 面板会就这一点给出提示(draw.restartHint)。
105
+ # 生效时机:开关值保存后**当场**挂/卸工具——peer 的 tools.register 返回 disposer
106
+ # (注销句柄),关掉就把工具摘掉,不用重启(机制与后果的完整论述见
107
+ # src/host/draw-register.ts 文件头注)。旧 Host 不返回句柄时才降级为 not-removable。
109
108
  drawEnabled: false
110
109
  # 出图默认模型(空 = 自动取目录里第一个出图模型)。
111
110
  # 注意只有目录内**平台声明**出图能力的模型才进候选:实测目录 35 条里
@@ -1141,7 +1141,7 @@ const inject = ["webServer"];
1141
1141
  * 产物反而引入第二份真源;这里用常量、由 test/config.test.mjs 钉住与
1142
1142
  * package.json 一致。
1143
1143
  */
1144
- const PLUGIN_VERSION = "0.6.5";
1144
+ const PLUGIN_VERSION = "0.7.0";
1145
1145
  /**
1146
1146
  * 默认值。刻意不含额度常数——理由见本文件头。
1147
1147
  * 面板头条是官方魔粒余额,次数口径的推算已在面板收敛时删除,这里若留着