dsh-connect-modelscope-token-plan 0.6.2 → 0.6.4

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,80 @@
26
26
  > **发版时**:顶部开 `## [X.Y.Z] — YYYY-MM-DD`,完整流程见
27
27
  > [RELEASING.md](RELEASING.md) §3(本文开头这段是**写作者规范**,那是**发布操作流程**)。
28
28
 
29
+ ## [0.6.4] — 2026-10-19
30
+
31
+ **出图:上游边缘抖动不再杀死任务**
32
+
33
+ - 轮询遇到 5xx / 429 / 408 不再判失败:与传输层抖动同一纪律,在本次等待预算内
34
+ 继续等,等不到才以 `pending` + `jobId` 收场(带 jobId 续查即可,不必重新出图)。
35
+ 此前同一种 CDN 边缘抖动只因表现为 503 就杀作业并进 60s 冷却。4xx 仍是终局。
36
+ - 出图作业表淘汰时跳过仍在续查窗内的作业(越过总放弃线的才优先丢)——能续查的
37
+ 作业被杀,两段式就残缺了;并发 pending 再多也不会杀到活作业。
38
+ - 余额缓存键改用完整令牌指纹(此前只取末 6 位,极小概率把一个身份的余额当成
39
+ 另一个的返回)。无界面变化。
40
+
41
+ **面板:本地计数的依赖说在明面上**
42
+
43
+ - 未开启「接入为 DSH 模型」时,「本地」区块现在给出一条提示说明计数从接入后
44
+ 开始记录——此前新装用户看到的是恒为 0/空的四块计数,与「插件坏了」不可区分;
45
+ 开了但没注册成功时提示改说注册状态;provider 状态读不到时不出这条,不替
46
+ Host 断言。
47
+
48
+ **卡片介绍:中英文同源**
49
+
50
+ - 卡片介绍(`locale/zh.json`、`locale/en.json`、`package.json` 的 `description`)此前是三份
51
+ 不同步的文本:中文里「默认关」挂在上下文窗口上(开关关的是模型接入,不是自动填入的窗口),
52
+ 英文另有一段与中文对不上的译文。现在三处与投稿拟稿逐字相同,中文为准、英文逐句对应。
53
+
54
+ > **实施过程(病灶取证、判据、为何两处不改)在
55
+ > [docs/archive/2026-10-19-0.6.4-process.md](docs/archive/2026-10-19-0.6.4-process.md)**。
56
+
57
+ ## [0.6.3] — 2026-10-19
58
+
59
+ **插件卡头部收敛:「魔搭模型接入」不再在同一屏出现两次**
60
+
61
+ - 卡内那个与外壳标题同名的大标题删掉了。插件名只由外壳在插件区正上方打印(取自
62
+ `locale/<lang>.json` 的 `meta.title`),卡内复述它会让同一个字符串在一屏出现两次、
63
+ 中间隔着外壳给所有插件的 32px 固定间隔,读起来像一个断成两半的表头。
64
+ - 原先独占一行、左半边空着的「更新于 / 刷新」并入 tab 行右侧;tab 条下方那条分隔线
65
+ 改由整行承担,激活 tab 的 2px 下划线正好压在线上。刷新按钮与「更新于」留在
66
+ `role="tablist"` 之外——读屏不会再把它们当成 tab 念。
67
+ - 卡内自带的 16/56 纵向内边距一并归零:它们叠加在外壳已给的 32px 段间距与 48px
68
+ 页底之上,使描述到 tab 之间空了 48px、页尾空了 104px。横向留白本来就由外壳负责,
69
+ 卡内一直是 0,本次不动。
70
+
71
+ **出图工具:失败文案不再替上游编原因,「停止」立即生效**
72
+
73
+ - 出图报 401/403 时不再一律附注「账号未完成邮箱验证/实名」:只有上游 body 点名了邮箱或
74
+ 实名(含英文 `email verification`)才照说,否则只说「令牌无效或无权访问该模型」。同一个
75
+ 401 也覆盖令牌过期,把实测遇到过一次的形态写成固定附注,等于替上游编造原因。
76
+ - 轮询遇到非 2xx 不再报「任务停在 RUNNING(非终态),已超过截止时间」:上一轮的读数
77
+ 只是上下文,本轮没读到就是没读到,HTTP 状态码与上游 body 照实带出。
78
+ - 等待中被撤下(插件卸载 / 热重挂)立即以 `pending` 收场并交回 `jobId`,不再等满等待
79
+ 预算;停止也能中止**在途的轮询请求**,不再只是中断本地 sleep。
80
+ - 出图作业表改由装配处统一创建与销毁:卸载后不再残留作业记录,`jobId` 续查读的也
81
+ 始终是同一张表。`jobId` 只在本次挂载内有效,pending 提示现在把这点写明(重启 DSH
82
+ 或重挂插件后要重新提交)。一次出图调用的硬超时不再可能比 `drawTimeoutMs` 上限多出 10 秒。
83
+ - 上游任务 FAILED 且响应体里**真写了**原因时照实报出(此前一律说「未给出失败原因」,
84
+ 把 `sensitive content detected` 这类 body 一起吞掉),且失败响应体一律附上——这是
85
+ 全函数唯一不附 body 的分支。
86
+
87
+ **面板与胶囊:读不到的时候不再假装知道**
88
+
89
+ - 面板读失败时错误行照常显示,不再要求「上一轮的数据还在」;数据还在时另起一句
90
+ 「下面是上次读到的数据」。此前首屏成功过、Host 随后挂掉时,界面是「上一轮的数字
91
+ + 完好的状态簇 + 一句沉默」:故障窗口越长,越像一份可信的实时读数——胶囊在同一
92
+ 情形下是挂「上次数据」标的,同一种「读不到」两种待遇。
93
+ - 模型目录读不到时 provider 卡现在说明原因。此前是「已注册 · 0 个模型」加一片空白:
94
+ 0 个模型是真的,而「为什么是 0」没人说,旧 roster 其实还在服务。
95
+ - 对话框胶囊只在**本帧真有读数**时才标「上次数据」。连续读失败时两个读数是「—」,
96
+ 此前仍给「—」挂这个标签——它在描述一份屏幕上并不存在的数据。
97
+ - 粘贴带前后空白的令牌不再存成一个发不出去的 `Authorization` 头(此前校验时忽略空白、
98
+ 落库时保留原值,面板还显示「已保存」)。
99
+
100
+ > **实施过程(病灶取证、判据、为何不抵消外壳间距)在
101
+ > [docs/archive/2026-10-19-0.6.3-process.md](docs/archive/2026-10-19-0.6.3-process.md)**。
102
+
29
103
  ## [0.6.2] — 2026-10-18
30
104
 
31
105
  **插件命名与卡片文案以「模型接入」为主题重排**
package/README.md CHANGED
@@ -1,70 +1,66 @@
1
1
  # dsh-connect-modelscope-token-plan
2
2
 
3
- 把魔搭社区(modelscope.cn)**API-Inference 免费额度**的本地用量面板接入 DeepSeek Harness 的 Plugins 页(插件卡内联,三个 tab:额度 / 模型 / 接入),并可选把魔搭注册成 **DSH 的 LLM provider**(id `modelscope-token-plan`,直连 `apiBase`,OpenAI 兼容)——注册后魔搭模型进入 DSH 模型选择器,可直接对话调用。见 [docs/PROVIDER-M4.md](docs/PROVIDER-M4.md)。
3
+ 把魔搭社区(modelscope.cn)**API-Inference 免费额度**的面板接进 DeepSeek Harness 的 **Plugins 页**(插件卡内联,三个 tab:额度 / 模型 / 接入),并可选把魔搭模型注册成 **DSH 的 LLM provider**(id `modelscope-token-plan`,OpenAI 兼容)——注册后魔搭模型进 DSH 模型选择器,可直接对话调用;还能开**出图工具**做文生图 / 图生图。
4
4
 
5
- 姊妹插件:`dsh-connect-sensenova-token-plan`、`dsh-connect-agnes-token-plan`(同族结构,受控复制)。
5
+ 姊妹插件:`dsh-connect-sensenova-token-plan`、`dsh-connect-agnes-token-plan`(同族结构)。
6
6
 
7
- **状态:M4 已落地(provider 注册 + 面板「接入为 DSH 模型」),离线测试与 typecheck/build 全绿。**
7
+ > **三条先知道(省得装完才发现)**
8
+ >
9
+ > 1. **只要一枚 API Key,不要登录**:魔搭访问令牌(形如 `ms-…`,在魔搭「个人中心 > 访问令牌」生成),无 OAuth、无密码、无刷新。`MODELSCOPE_API_KEY` 环境变量只在 DSH 启动时读一次、且优先级高于面板保存值——所以**最省事是直接在面板「接入」tab 保存令牌**(立即生效,不依赖重启)。
10
+ > 2. **面板头条是官方魔粒余额(真实值)**;本地计数只统计**经本插件的调用**,且**按 profile(客户端)分段**——桌面端与 Web 端各记各的账、互不可见(`npm run doctor` 并排列出各 profile)。它不是跨端总量。
11
+ > 3. **接入 provider 与出图工具都默认关**:不打开时插件就是个额度看板;打开任一项失败,面板照常用、只是该模块缺席。
8
12
 
9
13
  ## 面板长什么样
10
14
 
11
15
  三个 tab:**额度 / 模型 / 接入**。
12
16
 
13
- **「额度」tab**——头条是官方魔粒余额(真实值),下面接本地调用分布与趋势图:
17
+ **「额度」tab**——头条是官方魔粒余额(真实值),下面接本地调用分布与趋势图(竖向柱状,可切**显示天数**:默认 / 7 / 14 / 30 / 90):
14
18
 
15
19
  ![「额度」tab:魔粒余额与本地调用](assets/tab-account-and-usage.png)
16
20
 
17
- 趋势图是**竖向柱状图**(每根柱顶标出当天次数、0 次不标;轴约 7 个日期刻度且不带年份),区块内可切**显示天数**(默认 / 7 / 14 / 30 / 90)。这个选择器只决定「画多少天」,与决定「能回看多久」的**保留窗口**(`retentionDays`,默认 90 天)是两件事——显示窗口恒被夹在保留窗口之内,所以不会出现「把已剪掉的日子画成 0」那种与「那天真的没调用」无法区分的形状。选择保存在 `usage-prefs.json`(按 profile 分段)。
21
+ - 本地计数只回答官方余额答不了的问题——按模型分布、本地趋势、429 事件流;它**只读不到时画「—」**,绝不会把「不知道」画成 0。
22
+ - 本地计数**按 profile 分段**(本客户端口径):桌面端与 Web 端是两个 profile,各有各的状态目录,互不可见;直连魔搭的其它客户端不计入。
18
23
 
19
- **「模型」tab**——模型按**厂商分组**(组头可折叠、带数量与已选数,「推荐」徽标挂在组头、推荐的三家置顶),组内一行一个魔搭模型、勾选即启用,顶部可搜索;开关打开即把魔搭注册为 DSH provider:
24
+ **「模型」tab**——模型按厂商分组、勾选即启用,顶部可搜索;开关「接入为 DSH 模型」即把魔搭注册为 DSH provider,模型进入 DSH 选择器:
20
25
 
21
26
  ![「模型」tab:接入为DSH 模型与模型清单](assets/tab-model-list.png)
22
27
 
23
- 同一 tab 下方另有**「出图工具」**卡(v0.2 起):开启后 Host 给 agent 注册
24
- `modelscope_draw_image`,支持文生图、图生图与多图编辑。**它与「接入为 DSH 模型」
25
- 是两条独立开关**,且开关值立即保存、而工具的**挂载发生在下次 Host 启动**(peer 的
26
- tools registry 没有 unregister)。实测协议(异步两段式、必需头、提交态不可信、
27
- 计费与耗时口径)见 [docs/SPIKE-image-gen.md](docs/SPIKE-image-gen.md)。
28
-
29
- 工具本身也是**两段式**的(v0.4 起):一次调用只等 `drawWaitMs`(默认 90 秒)——
30
- 图好了直接返回,没好就返回 `status: pending` 与一个 `jobId`,**带上它再调用即可
31
- 继续查询**(只查询、不重复出图)。上游排队实测 1–15 分钟,此前一次调用会一直
32
- await 到出图为止,整轮对话都得陪着等。
33
-
34
- ![「模型」tab:出图工具卡(能力徽标、自动选择、重启提示)](assets/tab-model-draw.png)
35
-
36
- **卡片最底**另有**「上下文窗口覆盖」**选择器(不覆盖 / 128K / 256K / 512K / 1M):
37
- 魔搭目录 `/v1/models` 不返回窗口字段,Host 侧按四级真值链给每个模型定窗口——
38
- ① 目录真声明了就用目录值(不受任何下层影响);② 用户在下面选一个档位(保存即
39
- 重注册);③ 没设覆盖才查内置策展表(`context-catalog.ts`,来自 models.dev 各厂 host
40
- 交叉比对的同 id 主流窗口,2026-10-16 核对,含 DeepSeek-V4 全系 1M、
41
- Qwen3.5/Qwen3.8-27B/Step 3.x 256K、GLM-5.2 1M 等 19 个当前目录 id);④ 都没有才落
42
- 128K 兜底。**覆盖压过策展表**——插件自带的那份是第三方交叉真值,用户要改就改得动。
43
- 档位落盘在 `provider.json` 的 `contextWindow` 键(v1 形状的新增可选键,旧构建宽容读),
44
- roster 每行标出该模型当前生效的窗口,数字按**上游写出来的进制**显示(目录写 `128000`
45
- 就是 `128K`、档位写 `131072` 也是 `128K`,不做进制换算;策展表来的值带 `*` 号与悬停
46
- 说明——平台未亲口声明)。
28
+ - 同一 tab 下方有**「出图工具」**卡:开启后支持文生图、图生图与多图编辑。它与「接入为 DSH 模型」是两条独立开关,开关值立即保存、**工具的挂载发生在下次 Host 启动**。出图是异步的:一次没出好会返回 `status: pending` 与 `jobId`,带上它再调一次即可续查(不必重出)。协议细节见 [docs/SPIKE-image-gen.md](docs/SPIKE-image-gen.md)(2026-10-10 实测,v0.2 起已实现)。
29
+ - 卡片底部有**「上下文窗口覆盖」**选择器(不覆盖 / 128K / 256K / 512K / 1M):魔搭目录不返回窗口字段,插件按「目录声明 → 你选的档位 → 内置策展表(models.dev 交叉真值)→ 128K 兜底」四级兜底;覆盖压过策展表。档位来源与「为什么不给输出上限内置策展表」见 [docs/PROVIDER-M4.md](docs/PROVIDER-M4.md)。
30
+
31
+ **「接入」tab**——粘贴一枚 `ms-…` 访问令牌即完成凭据配置(令牌永不入日志、永不进插件目录)。没有令牌时面板会提示你到这里来填。
47
32
 
48
33
  ## 对话框胶囊(v0.2 起)
49
34
 
50
- 注册 provider 之后,输入框工具栏会多一枚胶囊(用户不需要打开 Plugins 页就能看见额度):
35
+ 注册 provider 之后,输入框工具栏会多一枚用量胶囊,不用打开 Plugins 页也能看到额度:
51
36
 
52
37
  ```
53
38
  魔搭 · 143 魔粒 · 今日本地 7 次
54
39
  ```
55
40
 
56
- - **前半**是官方魔粒余额(`balance.available`,真实值,与面板头条同一个数);**后半**是今日本地调用次数(Host 侧本地日桶,只统计经本插件的调用)。两个数字来源不同,所以后段必须带「本地」——官方接口没有「今日」这一项,不写清就会被读成官方计数。
57
- - **只在会话当前选中的 provider 是 `modelscope-token-plan` 时出现**(读宿主 session 作用域的 `modelSelection` 投影),切到别的 provider 即整块消失,且一个请求都不发。
58
- - **没有百分比、没有黄色档**。魔粒余额是**存量**、官方没有发布任何上限或低额阈值:画进度条或定一个「低于 N 魔粒算黄」的阈值,都是给一个没人公布过的分母配数。三档只有绿(读数是正数)/ 红(官方读数确实是 0)/ 灰(读不到)。
59
- - **读不到 ≠ 0**(与面板同一条纪律):读不到时该段渲染「—」,0 只在官方真的给了 0 时出现并走告警色;读失败保留上一次的数值,并标「上次数据」(阈值 = Host 声明的 `pollSeconds` × 3)。tooltip 里带余额时间、失败原文与本地计数缺失的原因。
41
+ 实际长这样(输入框工具栏,不用开 Plugins 页):
42
+
43
+ ![对话框用量胶囊:官方魔粒余额 + 今日本地调用次数](assets/modelscope-usage-capsule.png)
44
+
45
+ - 前半是官方魔粒余额(与面板头条同一个数);后半是**今日本地调用次数**(只统计经本插件的调用)——两个数字来源不同,所以后段必须带「本地」。
46
+ - **只在当前选中的 provider 是 `modelscope-token-plan` 时出现**,切到别的 provider 整块消失、不发任何请求。
47
+ - **没有百分比、没有黄色档**:魔粒是存量、官方没有发布上限或低额阈值,造进度条 / 阈值都是给沒人公布过的分母配数。三档只有绿(正)/ 红(官方确为 0)/ 灰(读不到)。
48
+ - **读不到 ≠ 0**:读不到时该段渲染「—」,0 只在官方真的给 0 时出现并走告警色;读失败保留上次数值并标「上次数据」。
49
+
50
+ ## 安装
60
51
 
61
- ## 三条事实(写代码前先认清)
52
+ 1. 在 DSH「插件」页搜索 `dsh-connect-modelscope-token-plan` 点击安装,或运行:
62
53
 
63
- 1. **官方有「魔粒」余额端点**:`GET {siteBase}/openapi/v1/magicubes/balance`(Bearer 访问令牌,匿名 401),返回 `{success, data:{total_balance, available_balance, frozen_amount}}`(2026-10-04 实测,见 [docs/SPIKE.md](docs/SPIKE.md))。推理响应本身仍不带任何额度头。
64
- 2. **本地计数是辅助口径**:面板头条是官方魔粒余额;本地计数只回答官方余额答不了的问题——按模型分布、本地趋势、429 事件流(只统计经本插件的调用,直连魔搭的其它客户端不计入)。次数口径的「推算剩余/参考上限」已从面板移除(官方改魔粒计费后两个单位并排是误导),相应的 `dailyQuotaTotal` / `dailyQuotaPerModel` 两个社区快照常数也从配置里一并删除了——面板不消费它们,留着只会让人以为能算「还剩几次」。
65
- 3. **凭据只有一把静态钥匙**:魔搭访问令牌(个人中心生成,形如 `ms-…`),无 OIDC、无密码、无 refresh。默认读 DSH 凭据服务已有的 `MODELSCOPE_API_KEY` 引用;`MODELSCOPE_API_KEY` 环境变量只在启动时读一次,且优先级高于存储值(坑见下段);令牌永不入日志、永不进插件目录。
54
+ ```bash
55
+ dsh plugin --profile web add dsh-connect-modelscope-token-plan # Web 端
56
+ dsh plugin --profile desktop add dsh-connect-modelscope-token-plan # 桌面端
57
+ ```
66
58
 
67
- **凭据分层有个坑(2026-10-05 实测)**:DSH 凭据服务的优先级里,**启动时的环境快照压过存储文件**,而那个快照**在启动时冻结**(不是活的 `process.env`)。两个后果:①在 DSH 启动**之后**改环境变量——包括 Windows 用户级变量——**不会被看见**,改完必须**完全重启 DSH**(退出全部 `DeepSeek Harness` 进程,不只是关窗口)才进快照;②重启让环境变量生效后,它会**反过来压过面板里保存的值**,而面板仍显示「已保存」。**最省事的做法是直接在面板里保存令牌**(立即生效,且不依赖重启)。
59
+ 2. **完全退出 DSH(含托盘)再启动**。
60
+
61
+ 3. 打开 DSH 的 **Plugins 页**,找到本插件的插件卡(面板是页内联卡片,**不在侧边栏**)。
62
+
63
+ > 装到 npm 的那一份**不含 `docs/`**(`package.json` 的 `files` 白名单只带 `lib/` / `client.js` / `assets/` / `README.md` / `CHANGELOG.md` / `LICENSE` 等),下文指向 `docs/*` 的链接在已安装副本上打不开,请到 GitHub 仓库看。
68
64
 
69
65
  ## 免费额度怎么算(2026-10 快照,以官方为准)
70
66
 
@@ -73,32 +69,43 @@ roster 每行标出该模型当前生效的窗口,数字按**上游写出来
73
69
  - 额度按天重置;耗尽返回 429。
74
70
  - `/v1/models` 免认证可读,**不消耗额度**。
75
71
 
76
- 上面这些数字来自社区公开信息,非官方数据,官方调整后需手动更新本节;**它们只用于解释
77
- 「额度怎么算」,不参与任何计算**——面板不消费次数上限,「还剩几次」算不出来(见下节)。
72
+ 上面这些数字来自社区公开信息、非官方数据,官方调整后需手动更新本节;**它们只用于解释「额度怎么算」,不参与任何计算**——面板不消费次数上限,「还剩几次」算不出来。
78
73
 
79
- ## 文档地图
74
+ ## 运维诊断:这台机器现在开没开 provider?
80
75
 
81
- | 何时 | 查 |
82
- |---|---|
83
- | 「上游到底有没有额度信号」 | [docs/SPIKE.md](docs/SPIKE.md)(实测记录 + 结论) |
84
- | 「能不能接生图、协议是什么」 | [docs/SPIKE-image-gen.md](docs/SPIKE-image-gen.md)(2026-10-10 实测;**v0.2 起已实现**,承上表体例) |
85
- | provider 怎么接入(写代码前) | [docs/PROVIDER-M4.md](docs/PROVIDER-M4.md)(实现契约) |
86
- | **这版是怎么做出来的、踩过什么坑** | [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md)(0.1.0 / M4 实施过程档案) |
87
- | v0.2.0 的十二轮修复细节(已冻结,非入门路径) | [docs/archive/2026-10-05-closeout.md](docs/archive/2026-10-05-closeout.md) |
88
- | 上游端点全景 / 第三方方案对比 | [docs/REFERENCES.md](docs/REFERENCES.md)、[docs/REFERENCE-modelsdev.md](docs/REFERENCE-modelsdev.md) |
89
- | v1 要做什么、做到哪了 | [docs/ROADMAP.md](docs/ROADMAP.md) |
90
- | Host/Client 两半怎么分 | [docs/PROVIDER-M4.md](docs/PROVIDER-M4.md)(§14 红线)+ [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md);本仓库暂无独立的 `ARCHITECTURE.md` |
91
- | 开发者校验工具(变异测试 / 覆盖率) | [tools/dev/README.md](tools/dev/README.md);发布流程见 [RELEASING.md](RELEASING.md) |
76
+ provider / 出图 / 上下文窗口的生效值存在插件私有状态文件里,不在任何配置或路由上,按 profile 分段。查「到底开没开」用 doctor,它只读状态文件、不碰凭据,Host 没起也能跑:
77
+
78
+ ```bash
79
+ npm run doctor # 人读:每个 profile 的 provider / draw 开关与模型清单
80
+ npm run doctor:json # 机器读:JSON(可进你的巡检 / 工单脚本)
81
+ ```
92
82
 
93
83
  ## 诚实声明
94
84
 
95
- - 面板头条是**官方魔粒余额**(真实值);本地计数只统计**经本插件的调用**——它回答的是分布/趋势/429 事件流,不是余额。**它是「本客户端」的口径**:注册 provider 之后该客户端的全部魔搭调用都经本插件,但**不跨客户端**——桌面端与 Web 端是两个 profile,各有各的状态目录(`$DSH_HOME/state/<profile>/<插件名>/usage.json`),各记各的账、互不可见;直连魔搭的其它客户端同样不计入。`npm run doctor` 会把每个 profile 的用量并排列出来,这是「另一个客户端的数字去哪了」的答案;迁移窗口期(旧共享目录与 profile 目录并存)它也会把旧共享布局以 `shared (legacy)` 作用域报出。次数口径的「推算剩余」已从面板移除(官方改魔粒计费后两个单位并排是误导),相应地`quota.daily` 只剩 `usedLocal` 一个数——没有 `limit`,也没有推算出的「剩余」。这个数还有第三种答案:**本地计数文件读不到时它是 `null`,面板渲染「—」**——「今日 0 次」是可信的日常值,把「我不知道」画成它会让人以为今天没调用过(`usage.json` 损坏、形状不对、或被更新的构建写过都是这种情形,原因会在面板上方的漂移提示里说明)。
96
- - **本地计数的口径与 harness 全局账本不同,且刻意不合并**。DSH 自己有 `@linxin666/dsh-usage`,按 `days→provider→model` 记成功调用的 token;它只订阅 session 的 `assistant/message`,**失败调用一次都不记**。所以本插件只做两件全局账本答不了的事:①「经本插件的调用分布与趋势」;②**429/错误事件流**——这是本插件的独家数据(官方只给总额度余额,不给「哪个模型刚才在限频」)。成功的 token 总量不在此重复记账(详见 `src/host/usage-observer.ts` 文件头)。
97
- - 429/错误事件的分类读的是 **`llm-error-fix` 重分类之后**的 code:被误判成 QUOTA 的 rpm 限频在事件流里记作 `rate_limit`,与退避策略说同一件事。观察层套在重分类层**之外**,顺序不可反。
98
- - **单模型「额度耗尽」是推断,不是读数**:官方只有账户级魔粒余额,没有 per-model 额度端点。某模型最近一次真耗尽(quota 类 429 事件,重分类后的口径;瞬时限频不算)落在推断窗口内(`exhaustionWindowMinutes`,默认 60 分钟,5–1440 可调)时,picker 不再提供它、面板 roster 灰显且禁勾选;事件移出窗口即恢复。**恢复不可观测是已知代价**——成功的调用不发事件,额度若在窗口内恢复(按天重置恰好如此),模型最多灰显满一个窗口:方向保守(不提供注定 429 的模型),且灰显带着原因,不是静默消失。判定规则与接线见 [docs/PROVIDER-M4.md](docs/PROVIDER-M4.md) §15。
99
- - **计数口径**:每次流结束记一次 call,**含失败、含用户中断**(魔搭按次数计费,限频掉的请求同样是一次调用);token 数上游没给时是 `null` 而非 0。偏差方向是**按流计次**:重试不在流内部发生,而是在流消费完后由 `agent/request-error` 瀑布决定重发,因而**每次重试各记一次**——按「魔搭按次数计费」的口径这反而是对的(每次重发都真打了一次上游)。
100
- - **provider 默认关**(opt-in,与姊妹插件一致):面板「模型」tab 的「接入为 DSH 模型」开关翻转即生效,面板保存值优先于 `cordis.patch.yml` 的 `registerProvider` 默认。
101
- - **已知限制:`reasoning` 恒为 false**。魔搭 API-Inference 是多模型代理,是否吃 `reasoning_effort` 取决于背后那个模型,本插件保守默认不发该参数(模型用自己的默认),也不提供思考强度选择器。**原理由「发错档位会整条请求 400」已在 2026-10-05 真机探测中证伪**:`reasoning_effort` 传 `"bogus"` 同样返回 200,值不被校验。真实障碍是无法区分「生效」与「静默忽略」(都是 200),且该端点会在真响应与空壳 200 之间摇摆——档位表做不出来,不是没做。见 [docs/PROVIDER-M4.md](docs/PROVIDER-M4.md) §4。
102
- - **输出上限默认是 harness 的 32768,不是魔搭声明的数**。魔搭 `/v1/models` 从不返回输出上限字段(实测每条只有 `id`/`object`/`owned_by`/`created`),而本插件对输出上限的策略是「目录声明了才声明」——两条合起来:**每一个**模型的 descriptor 都不带 `maxTokens`,harness 于是用它的 `defaultMaxTokens ?? 32768` 兜底。32768 不是魔搭声明的数、也不是实测过的数,它只是 harness 的缺省值;目录给不出真值时,只有你知道某个模型实测能吐多少。出口是 `cordis.patch.yml` 的 `maxTokensOverrides`(**精确**模型 id → 正整数),优先级 `覆盖 > 目录声明 > 不声明`;面板模型清单行用「最大输出」徽章显示**生效值**(来源是覆盖还是目录写在 tooltip 里),**没带徽章 = 正在吃 32768 兜底**——「没显示」不等于「没上限」,这是本面板最容易读反的一处。**为什么不像上下文窗口那样内置一张策展表**:窗口有 models.dev 的交叉真值可引,输出上限平台从不声明、也没对它做过逐模型实测,策展一张「猜每个模型能吐多少」的表就是在编数字。
103
- - 429 响应形状未实测(不值得烧额度去触发),解析按 OpenAI 惯例兼容,漂移时面板会给 `shapeWarnings`。
104
- - 面板「模型」tab 的每个模型都外链到其魔搭详情页(`https://www.modelscope.cn/models/{owner}/{model}`):魔粒单价只在网页展示、不在官方 API,所以这只是一个**人工核对用的跳转,不是数据源**(「为什么没有按模型消耗 API」见 [docs/REFERENCES.md](docs/REFERENCES.md))。
85
+ - 面板头条是**官方魔粒余额**(真实值);本地计数只统计**本客户端(本 profile)内经本插件的调用**——它回答分布/趋势/429 事件流,不是余额,且桌面端与 Web 端各记各的、互不可见(迁移窗口期旧共享布局以 `shared (legacy)` 报出)。读不到时渲染「—」,不是 0。
86
+ - **本地计数与 harness 全局账本刻意不合并**:DSH 自己的 `@linxin666/dsh-usage` 只记成功调用的 token、且失败一次都不记;本插件只补它答不了的两件事——① 经本插件的分布与趋势;② **429 / 错误事件流**(官方只给总额度余额,不给「哪个模型刚才在限频」,这是本插件的独家数据)。
87
+ - 429 / 错误事件读的是 `llm-error-fix` 重分类之后的 code;观察层套在重分类层之外,顺序不可反。
88
+ - **单模型「额度耗尽」是推断,不是读数**:官方只有账户级魔粒余额,没有 per-model 额度端点;最近一次真耗尽落在推断窗口内时 picker 不再提供它、面板灰显禁勾选,事件移出窗口即恢复(恢复不可观测是已知代价)。判定见 [docs/PROVIDER-M4.md](docs/PROVIDER-M4.md) §15。
89
+ - 计数口径:每次流结束记一次 call,**含失败、含用户中断**;token 数上游没给时是 `null` 而非 0。重试各记一次(魔搭按次数计费,每次重发都真打了一次上游;完整论述见 `src/host/usage-observer.ts`)。
90
+ - **provider 默认关**(opt-in):面板「模型」tab 的「接入为 DSH 模型」开关翻转即生效,面板保存值优先于 `cordis.patch.yml` 的默认。
91
+ - **已知限制:`reasoning` 恒为 false**:本插件保守默认不发 `reasoning_effort`(模型用自己的默认),也不提供思考强度选择器——值不被校验、且无法区分「生效」与「静默忽略」。完整论证见 [docs/PROVIDER-M4.md](docs/PROVIDER-M4.md) §4。
92
+ - **输出上限默认是 harness 的 32768,不是魔搭声明的数**:魔搭 `/v1/models` 从不返回输出上限字段,本插件策略是「目录声明了才声明」,所以每个模型的 descriptor 都不带 `maxTokens`,harness 用缺省 32768 兜底;面板「最大输出」徽章显示生效值,**没带徽章 = 正在吃 32768 兜底**。精确覆盖在 `cordis.patch.yml` 的 `maxTokensOverrides`。为什么不像上下文窗口那样内置策展表:平台从不声明、也没逐模型实测,策展一张「猜能吐多少」的表就是在编数字(详见 [docs/PROVIDER-M4.md](docs/PROVIDER-M4.md))。
93
+ - 次数口径的「推算剩余 / 参考上限」已从面板移除(官方改魔粒计费后两个单位并排是误导),配置里的 `dailyQuotaTotal` / `dailyQuotaPerModel` 两个社区快照常数也一并删除——面板不消费它们,留着只会让人以为能算「还剩几次」。
94
+ - 429 响应形状未实测,解析按 OpenAI 惯例兼容,漂移时面板给 `shapeWarnings`。
95
+ - 面板「模型」tab 的每个模型外链到其魔搭详情页(单价只在网页展示、不在官方 API),所以只是**人工核对用的跳转,不是数据源**。
96
+
97
+ ## 文档
98
+
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/) — 已冻结的逐版修复档案
106
+
107
+ AI 协作会话请先读 [AGENTS.md](AGENTS.md)(验证入口、红线与文档地图);开发者校验工具(变异测试 / 覆盖率)见 [tools/dev/README.md](tools/dev/README.md);发布流程见 [RELEASING.md](RELEASING.md)。
108
+
109
+ ## 许可证
110
+
111
+ MIT License,全文见 [LICENSE](LICENSE)。
package/client.js CHANGED
@@ -29,7 +29,6 @@ var dsh_connect_modelscope_token_plan_client = (function() {
29
29
  "events",
30
30
  "trend",
31
31
  "models",
32
- "provider",
33
32
  "shapeWarnings",
34
33
  "quotaError"
35
34
  ]);
@@ -136,11 +135,11 @@ var dsh_connect_modelscope_token_plan_client = (function() {
136
135
  var zh, en;
137
136
  var init_i18n = __esmMin((() => {
138
137
  zh = {
139
- "panel.title": "魔搭模型接入",
140
138
  "panel.refresh": "刷新",
141
139
  "panel.updated": "更新于 {time}",
142
140
  "panel.loading": "加载中…",
143
141
  "panel.error": "读取失败:{error}",
142
+ "panel.staleData": "(下面是上次读到的数据)",
144
143
  "panel.configError": "插件配置有误:{error}",
145
144
  "panel.networkError": "无法连接本机 Host,通常是临时故障,下一轮自动刷新即可恢复。",
146
145
  "panel.timeout": "请求超时,下一轮自动刷新即可恢复。",
@@ -175,6 +174,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
175
174
  "quota.rowTokens": "{tokens} token",
176
175
  "quota.note": "本地口径:只统计本客户端经本插件的调用(桌面端与 Web 端各记各的);直连魔搭的其它客户端不计入。消耗总量以官方魔粒余额为准。",
177
176
  "quota.unavailable": "本地计数本次读不到(原因见上方提示)",
177
+ "quota.countingOff": "本地计数只统计接入后的调用:当前未开启「接入为 DSH 模型」(开关在「模型」tab),开启后开始记录。",
178
+ "quota.countingPending": "「接入为 DSH 模型」已开启但尚未注册成功(见「模型」tab 状态),注册生效后本地计数开始记录。",
178
179
  "probe.validity": "验令牌(零额度)",
179
180
  "probe.validOk": "令牌有效(HTTP {status})",
180
181
  "probe.fail": "失败:{error}",
@@ -288,11 +289,11 @@ var dsh_connect_modelscope_token_plan_client = (function() {
288
289
  "composer.stale": "上次数据"
289
290
  };
290
291
  en = {
291
- "panel.title": "ModelScope Model Access",
292
292
  "panel.refresh": "Refresh",
293
293
  "panel.updated": "Updated {time}",
294
294
  "panel.loading": "Loading…",
295
295
  "panel.error": "Read failed: {error}",
296
+ "panel.staleData": "(showing the last data read successfully)",
296
297
  "panel.configError": "Plugin misconfiguration: {error}",
297
298
  "panel.networkError": "Cannot reach the local Host — usually transient, the next auto-refresh will recover.",
298
299
  "panel.timeout": "Request timed out; the next auto-refresh will recover.",
@@ -327,6 +328,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
327
328
  "quota.rowTokens": "{tokens} tokens",
328
329
  "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.",
329
330
  "quota.unavailable": "Local counters could not be read this time (reason in the notices above)",
331
+ "quota.countingOff": "Local counts come from calls made through this plugin's provider. \"Access as DSH models\" is off in the Models tab — counts start once it is on.",
332
+ "quota.countingPending": "\"Access as DSH models\" is on but the provider is not registered yet (see the Models tab); local counts start once registration takes effect.",
330
333
  "probe.validity": "Verify token (free)",
331
334
  "probe.validOk": "Token valid (HTTP {status})",
332
335
  "probe.fail": "Failed: {error}",
@@ -573,31 +576,24 @@ var dsh_connect_modelscope_token_plan_client = (function() {
573
576
  fontSize: 14,
574
577
  lineHeight: "22px"
575
578
  },
576
- headerBar: {
577
- flex: "none",
578
- background: "var(--dsw-alias-bg-base)",
579
- position: "relative",
580
- zIndex: 1
581
- },
582
- header: {
583
- display: "flex",
584
- alignItems: "center",
585
- gap: 12,
586
- padding: "16px 0 12px"
587
- },
588
579
  scroll: {
589
580
  flex: 1,
590
581
  minHeight: 0,
591
582
  overflowY: "auto",
592
583
  overflowX: "hidden"
593
584
  },
594
- content: { padding: "6px 0 56px" },
595
- tabBar: {
585
+ content: { padding: 0 },
586
+ tabRow: {
596
587
  display: "flex",
597
- gap: 4,
588
+ alignItems: "stretch",
589
+ gap: 12,
598
590
  borderBottom: "1px solid var(--dsw-alias-border-l1)",
599
591
  marginBottom: 4
600
592
  },
593
+ tabBar: {
594
+ display: "flex",
595
+ gap: 4
596
+ },
601
597
  tab: {
602
598
  appearance: "none",
603
599
  background: "none",
@@ -607,19 +603,15 @@ var dsh_connect_modelscope_token_plan_client = (function() {
607
603
  fontSize: 13,
608
604
  color: "var(--dsw-alias-label-secondary)",
609
605
  cursor: "pointer",
610
- outlineOffset: -2
606
+ outlineOffset: -2,
607
+ display: "flex",
608
+ alignItems: "center"
611
609
  },
612
610
  tabActive: {
613
611
  color: "var(--dsw-alias-label-primary)",
614
612
  fontWeight: 600,
615
613
  borderBottom: `2px solid ${BRAND}`
616
614
  },
617
- title: {
618
- margin: 0,
619
- fontSize: 20,
620
- fontWeight: 600,
621
- lineHeight: "28px"
622
- },
623
615
  updated: {
624
616
  color: "var(--dsw-alias-label-secondary)",
625
617
  fontSize: 12
@@ -630,7 +622,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
630
622
  alignItems: "center",
631
623
  gap: 12,
632
624
  flexWrap: "wrap",
633
- justifyContent: "flex-end"
625
+ justifyContent: "flex-end",
626
+ alignSelf: "center"
634
627
  },
635
628
  button: BUTTON,
636
629
  sectionTitle: {
@@ -1529,8 +1522,12 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1529
1522
  * 刻意**不画**「推算剩余/参考上限」进度条:官方已改为魔粒计费,「2000 次」
1530
1523
  * 是次数口径的社区快照,与魔粒余额并排展示是误导(README「三条事实」#2)。
1531
1524
  * 本地计数回答的是官方余额回答不了的问题:哪个模型在烧、何时撞的 429。
1525
+ *
1526
+ * `countingNote`(已翻译的提示文本,null = 不显示)由 PanelPage 给:本地计数的
1527
+ * 唯一生产者挂在注册 provider 后的适配器上,未接入时这张卡恒为 0/空——那件事
1528
+ * 必须说出口(2026-10-19 评审落地)。
1532
1529
  */
1533
- function LocalDailyCard({ snapshot, tt }) {
1530
+ function LocalDailyCard({ snapshot, countingNote = null, tt }) {
1534
1531
  const quota = snapshot.quota;
1535
1532
  const daily = quota?.daily ?? { usedLocal: null };
1536
1533
  const perModel = quota?.perModel;
@@ -1547,7 +1544,10 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1547
1544
  } }, tt("quota.unavailable")) : null, perModelUnknown ? null : h(ModelUsageTable, {
1548
1545
  rows: perModelRows,
1549
1546
  tt
1550
- }), h("div", { style: S.trendLegend }, tt("quota.note")));
1547
+ }), countingNote !== null ? h("div", { style: {
1548
+ ...S.formNote,
1549
+ color: "var(--dsw-alias-state-warn-primary)"
1550
+ } }, countingNote) : null, h("div", { style: S.trendLegend }, tt("quota.note")));
1551
1551
  }
1552
1552
  /** 今日单模型排行(相对最大者画条)。 */
1553
1553
  function ModelUsageTable({ rows, tt }) {
@@ -2622,6 +2622,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
2622
2622
  cancelled = true;
2623
2623
  };
2624
2624
  }, [hasDrawBlock, pollRevision]);
2625
+ const countingNote = !(hasProviderBlock || fetchedProvider !== null) ? null : provider.enabled !== true ? tt("quota.countingOff") : provider.registered !== true ? tt("quota.countingPending") : null;
2625
2626
  const [providerBusy, setProviderBusy] = useState(false);
2626
2627
  const [providerError, setProviderError] = useState(null);
2627
2628
  const writeErrorText = (reason, t) => reason instanceof Error && reason.name === "AbortError" ? t("common.timeout") : errorText(reason);
@@ -2772,6 +2773,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
2772
2773
  tt
2773
2774
  }, h(LocalDailyCard, {
2774
2775
  snapshot: snap,
2776
+ countingNote,
2775
2777
  tt
2776
2778
  })), h(SectionCard, {
2777
2779
  title: format(tt("section.trend"), { days: snap.trend?.days ?? 0 }),
@@ -2852,15 +2854,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
2852
2854
  return h("div", {
2853
2855
  style: S.page,
2854
2856
  "data-dsh-plugin": PANEL_ID
2855
- }, h("div", { style: S.headerBar }, h("div", { style: S.header }, h("h1", { style: S.title }, tt("panel.title")), h("span", { style: S.spacer }), h("span", { style: S.cluster }, data !== null ? h("span", { style: S.updated }, format(tt("panel.updated"), { time: isoTime(new Date(updatedAt).toISOString()) })) : null, failure !== null && data !== null ? h("span", {
2856
- style: S.error,
2857
- role: "status",
2858
- title: failure.message
2859
- }, format(tt("panel.error"), { error: failure.message })) : null, h("button", {
2860
- type: "button",
2861
- style: S.button,
2862
- onClick: () => void load()
2863
- }, tt("panel.refresh"))))), h("div", { style: S.scroll }, h("div", { style: S.content }, h("div", null, h("div", {
2857
+ }, h("div", { style: S.scroll }, h("div", { style: S.content }, h("div", null, h("div", { style: S.tabRow }, h("div", {
2864
2858
  style: S.tabBar,
2865
2859
  role: "tablist"
2866
2860
  }, h("button", {
@@ -2890,7 +2884,15 @@ var dsh_connect_modelscope_token_plan_client = (function() {
2890
2884
  ...activeTab === "access" ? S.tabActive : {}
2891
2885
  },
2892
2886
  onClick: () => setActiveTab("access")
2893
- }, tt("tab.access"))), data !== null && shapeWarnings.length > 0 ? h("div", {
2887
+ }, tt("tab.access"))), h("span", { style: S.spacer }), h("span", { style: S.cluster }, data !== null ? h("span", { style: S.updated }, format(tt("panel.updated"), { time: isoTime(new Date(updatedAt).toISOString()) })) : null, failure !== null ? h("span", {
2888
+ style: S.error,
2889
+ role: "status",
2890
+ title: failure.message
2891
+ }, format(tt("panel.error"), { error: failure.message })) : null, failure !== null && data !== null ? h("span", { style: S.updated }, tt("panel.staleData")) : null, h("button", {
2892
+ type: "button",
2893
+ style: S.button,
2894
+ onClick: () => void load()
2895
+ }, tt("panel.refresh")))), data !== null && shapeWarnings.length > 0 ? h("div", {
2894
2896
  style: S.formError,
2895
2897
  role: "status"
2896
2898
  }, format(tt("panel.shapeDrift"), { detail: shapeWarnings.join("; ") })) : null, activeTab === "quota" ? quotaBody() : activeTab === "models" ? modelsBody() : accessBody()))));
@@ -2978,10 +2980,17 @@ var dsh_connect_modelscope_token_plan_client = (function() {
2978
2980
  * ——失败退避或页面被挂起都会这样。取 3 个声明周期而不是 1 个:一次偶发失败就会
2979
2981
  * 拉长间隔,卡在 1 个周期上会让每一次抖动都把读数打成陈旧的。
2980
2982
  *
2981
- * `updatedAt === 0` 表示**从没读到过**,那不是「陈旧」而是「还没有」——调用方据此
2982
- * 决定不标;本函数只在有读数的前提下判断,故返回 false。
2983
+ * **`hasReading` 是承重输入**:`updatedAt > 0` 只说明**曾经**读到过,不说明屏幕上
2984
+ * 此刻有数字——读失败时轮询 hook 会 `setData(null)` 却保留 `updatedAt`
2985
+ * (`use-snapshot-polling.ts`),只看 updatedAt 就会在两个读数都是「—」时挂上
2986
+ * 「上次数据」,描述一份不在屏幕上的数据(2026-10-19 锐评实测)。调用方必须把
2987
+ * 「本帧有没有读数」传进来。
2988
+ *
2989
+ * `hasReading === false` 表示**从没读到过 / 这一帧没有读数**,那不是「陈旧」而是
2990
+ * 「还没有」——调用方据此不标;本函数据此返回 false。
2983
2991
  */
2984
2992
  function staleOf(input) {
2993
+ if (!input.hasReading) return false;
2985
2994
  if (input.error !== null && input.error !== void 0) return input.updatedAt > 0;
2986
2995
  return input.updatedAt > 0 && input.now - input.updatedAt > input.cadenceMs * STALE_CADENCES;
2987
2996
  }
@@ -3015,6 +3024,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
3015
3024
  const cadenceMs = statedCadenceMs(data?.pollSeconds, DEFAULT_CADENCE_MS);
3016
3025
  const stale = staleOf({
3017
3026
  error,
3027
+ hasReading: data !== null,
3018
3028
  updatedAt,
3019
3029
  now: Date.now(),
3020
3030
  cadenceMs
@@ -1,13 +1,13 @@
1
1
  //#region src/host/util.ts
2
- /** 读一个有限的正数,否则回退值。 */
3
- function num(value, fallback) {
2
+ /** 读一个有限的正数,否则回退值(调用方一律传 number 回退,返回值不再 any)。 */
3
+ function num(value, fallback = 0) {
4
4
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
5
5
  }
6
- /** 读一个非空字符串,否则回退值。 */
7
- function str(value, fallback) {
6
+ /** 读一个非空字符串,否则回退值(调用方一律传 string 回退,返回值收窄为 string)。 */
7
+ function str(value, fallback = "") {
8
8
  return typeof value === "string" && value.trim() !== "" ? value.trim() : fallback;
9
9
  }
10
- /** 读一个普通对象,否则 `{}`。 */
10
+ /** 读一个普通对象,否则 `{}`(返回值给具体形状:点号访问合法、赋值不再绕过检查)。 */
11
11
  function obj(value) {
12
12
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
13
13
  }
@@ -16,7 +16,7 @@ function errMsg(value) {
16
16
  return value instanceof Error ? value.message : String(value);
17
17
  }
18
18
  /** 原样读字符串(凭据专用:trim 是用户看不见的改动)。 */
19
- function verbatim(value, fallback) {
19
+ function verbatim(value, fallback = "") {
20
20
  return typeof value === "string" ? value : fallback;
21
21
  }
22
22
  /**
@@ -291,7 +291,20 @@ async function* reclassifyStream(source) {
291
291
  * 「点隐藏反而全放出来」。
292
292
  */
293
293
  const HIDE_ALL_MODELS = "__hide_all__";
294
- /** 钉死清单:任何新增顶层字段都必须先过 test/wire.test.mjs。 */
294
+ /**
295
+ * 钉死清单:任何新增顶层字段都必须先过 test/wire.test.mjs。
296
+ *
297
+ * **可选块不得进这份清单**——这不是排版洁癖,是 53c9902 的修复曾经被它架空的
298
+ * 事故的结论:`provider` 改成可选(Host 软失败时省略)之后仍留在这里,于是客户端
299
+ * 把「Host 按契约省略」当成契约破裂,先把 `DEGRADED_PROVIDER` 补进 body,再据此
300
+ * 判定「块在」,**兜底 `GET /provider` 一次都没跑**,还给用户挂一条假的漂移警告。
301
+ * `draw` 先可选、也先移出清单,所以它没有这个问题——两个块走同一条纪律,清单里
302
+ * 就必须同样缺席。
303
+ *
304
+ * 判据:`Snapshot` 接口里带 `?` 的顶层字段,这里**必须**没有。
305
+ * test/wire.test.mjs 机械解析该接口做三向交叉:必给字段集合 === 本清单、可选
306
+ * 字段 ∩ 清单 = ∅、可选字段集合 === 可选块名字清单——清单腐化或漏登记都判红。
307
+ */
295
308
  const SNAPSHOT_REQUIRED_KEYS = Object.freeze([
296
309
  "ok",
297
310
  "name",
@@ -305,7 +318,6 @@ const SNAPSHOT_REQUIRED_KEYS = Object.freeze([
305
318
  "events",
306
319
  "trend",
307
320
  "models",
308
- "provider",
309
321
  "shapeWarnings",
310
322
  "quotaError"
311
323
  ]);
@@ -1129,7 +1141,7 @@ const inject = ["webServer"];
1129
1141
  * 产物反而引入第二份真源;这里用常量、由 test/config.test.mjs 钉住与
1130
1142
  * package.json 一致。
1131
1143
  */
1132
- const PLUGIN_VERSION = "0.6.2";
1144
+ const PLUGIN_VERSION = "0.6.4";
1133
1145
  /**
1134
1146
  * 默认值。刻意不含额度常数——理由见本文件头。
1135
1147
  * 面板头条是官方魔粒余额,次数口径的推算已在面板收敛时删除,这里若留着
@@ -1441,4 +1453,4 @@ function isAdmitted(request, allowedHosts) {
1441
1453
  }
1442
1454
 
1443
1455
  //#endregion
1444
- export { errMsg as C, redactSecrets as D, pluginError as E, str as O, degrade as S, optional as T, parseAllowedList as _, resolveSettings as a, looksLikeRateLimit as b, LLM_PROVIDER_ID as c, imageGenModelIds as d, isVisionModel as f, enabledCountOf as g, rosterWithAvailability as h, name as i, verbatim as k, buildDescriptors as l, resolveDrawModelChoice as m, inject as n, LLM_API_KEY_NAME as o, normalizeEntry as p, isAdmitted as r, LLM_DISPLAY_NAME as s, PLUGIN_VERSION as t, drawCandidates as u, CODE as v, obj as w, reclassifyStream as x, hasHardQuotaWordingIn as y };
1456
+ export { verbatim as A, degrade as C, pluginError as D, optional as E, redactSecrets as O, reclassifyStream as S, obj as T, enabledCountOf as _, name as a, hasHardQuotaWordingIn as b, LLM_DISPLAY_NAME as c, drawCandidates as d, imageGenModelIds as f, rosterWithAvailability as g, resolveDrawModelChoice as h, isAdmitted as i, str as k, LLM_PROVIDER_ID as l, normalizeEntry as m, PLUGIN_VERSION as n, resolveSettings as o, isVisionModel as p, inject as r, LLM_API_KEY_NAME as s, DRAW_TIMEOUT_MAX_MS as t, buildDescriptors as u, parseAllowedList as v, errMsg as w, looksLikeRateLimit as x, CODE as y };