dsh-connect-modelscope-token-plan 0.6.3 → 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,34 @@
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
+
29
57
  ## [0.6.3] — 2026-10-19
30
58
 
31
59
  **插件卡头部收敛:「魔搭模型接入」不再在同一屏出现两次**
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
@@ -174,6 +174,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
174
174
  "quota.rowTokens": "{tokens} token",
175
175
  "quota.note": "本地口径:只统计本客户端经本插件的调用(桌面端与 Web 端各记各的);直连魔搭的其它客户端不计入。消耗总量以官方魔粒余额为准。",
176
176
  "quota.unavailable": "本地计数本次读不到(原因见上方提示)",
177
+ "quota.countingOff": "本地计数只统计接入后的调用:当前未开启「接入为 DSH 模型」(开关在「模型」tab),开启后开始记录。",
178
+ "quota.countingPending": "「接入为 DSH 模型」已开启但尚未注册成功(见「模型」tab 状态),注册生效后本地计数开始记录。",
177
179
  "probe.validity": "验令牌(零额度)",
178
180
  "probe.validOk": "令牌有效(HTTP {status})",
179
181
  "probe.fail": "失败:{error}",
@@ -326,6 +328,8 @@ var dsh_connect_modelscope_token_plan_client = (function() {
326
328
  "quota.rowTokens": "{tokens} tokens",
327
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.",
328
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.",
329
333
  "probe.validity": "Verify token (free)",
330
334
  "probe.validOk": "Token valid (HTTP {status})",
331
335
  "probe.fail": "Failed: {error}",
@@ -1518,8 +1522,12 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1518
1522
  * 刻意**不画**「推算剩余/参考上限」进度条:官方已改为魔粒计费,「2000 次」
1519
1523
  * 是次数口径的社区快照,与魔粒余额并排展示是误导(README「三条事实」#2)。
1520
1524
  * 本地计数回答的是官方余额回答不了的问题:哪个模型在烧、何时撞的 429。
1525
+ *
1526
+ * `countingNote`(已翻译的提示文本,null = 不显示)由 PanelPage 给:本地计数的
1527
+ * 唯一生产者挂在注册 provider 后的适配器上,未接入时这张卡恒为 0/空——那件事
1528
+ * 必须说出口(2026-10-19 评审落地)。
1521
1529
  */
1522
- function LocalDailyCard({ snapshot, tt }) {
1530
+ function LocalDailyCard({ snapshot, countingNote = null, tt }) {
1523
1531
  const quota = snapshot.quota;
1524
1532
  const daily = quota?.daily ?? { usedLocal: null };
1525
1533
  const perModel = quota?.perModel;
@@ -1536,7 +1544,10 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1536
1544
  } }, tt("quota.unavailable")) : null, perModelUnknown ? null : h(ModelUsageTable, {
1537
1545
  rows: perModelRows,
1538
1546
  tt
1539
- }), 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")));
1540
1551
  }
1541
1552
  /** 今日单模型排行(相对最大者画条)。 */
1542
1553
  function ModelUsageTable({ rows, tt }) {
@@ -2611,6 +2622,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
2611
2622
  cancelled = true;
2612
2623
  };
2613
2624
  }, [hasDrawBlock, pollRevision]);
2625
+ const countingNote = !(hasProviderBlock || fetchedProvider !== null) ? null : provider.enabled !== true ? tt("quota.countingOff") : provider.registered !== true ? tt("quota.countingPending") : null;
2614
2626
  const [providerBusy, setProviderBusy] = useState(false);
2615
2627
  const [providerError, setProviderError] = useState(null);
2616
2628
  const writeErrorText = (reason, t) => reason instanceof Error && reason.name === "AbortError" ? t("common.timeout") : errorText(reason);
@@ -2761,6 +2773,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
2761
2773
  tt
2762
2774
  }, h(LocalDailyCard, {
2763
2775
  snapshot: snap,
2776
+ countingNote,
2764
2777
  tt
2765
2778
  })), h(SectionCard, {
2766
2779
  title: format(tt("section.trend"), { days: snap.trend?.days ?? 0 }),
@@ -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.3";
1144
+ const PLUGIN_VERSION = "0.6.4";
1145
1145
  /**
1146
1146
  * 默认值。刻意不含额度常数——理由见本文件头。
1147
1147
  * 面板头条是官方魔粒余额,次数口径的推算已在面板收敛时删除,这里若留着
package/lib/index.js CHANGED
@@ -1738,6 +1738,24 @@ function createInferenceClient({ settings, tokenStore, deps = {}, logger }) {
1738
1738
  }
1739
1739
  return Array.from(new Set(tasks));
1740
1740
  };
1741
+ /**
1742
+ * 令牌的稳定指纹(FNV-1a,32 位 hex)——只用作余额缓存的键。
1743
+ *
1744
+ * **不用 `token.slice(-6)` 那种末位截断**:两个不同令牌末 6 位相同的概率在
1745
+ * 1/16^7 量级,虽低但非零,而代价是一个 TTL(cacheSeconds,默认 60s)内把
1746
+ * A 的身份的余额当 B 的返回——缓存键的正确性不该交给运气。指纹不需要保密、
1747
+ * 不进日志(脱敏正则的形状与它无关),完整的哈希零成本。
1748
+ * @param {string} token - 访问令牌。
1749
+ * @returns {string} 8 位 hex 指纹。
1750
+ */
1751
+ function tokenFingerprint(token) {
1752
+ let hash = 2166136261;
1753
+ for (let i = 0; i < token.length; i++) {
1754
+ hash ^= token.charCodeAt(i);
1755
+ hash = Math.imul(hash, 16777619);
1756
+ }
1757
+ return (hash >>> 0).toString(16).padStart(8, "0");
1758
+ }
1741
1759
  return {
1742
1760
  /**
1743
1761
  * 模型目录:GET apiBase/models,免认证、零额度,coalesced 缓存
@@ -1831,13 +1849,14 @@ function createInferenceClient({ settings, tokenStore, deps = {}, logger }) {
1831
1849
  /**
1832
1850
  * 官方「魔粒」余额:GET siteBase/openapi/v1/magicubes/balance(Bearer
1833
1851
  * 令牌;2026-10-04 实测可用,匿名 401,SPIKE.md §魔粒)。coalesced 缓存
1834
- * cacheSeconds,缓存键带令牌指纹——换令牌 ≤1 个 TTL 内必然读到新身份。
1852
+ * cacheSeconds,缓存键带令牌指纹(FNV-1a 完整哈希,不是末位截断——
1853
+ * 换令牌 ≤1 个 TTL 内必然读到新身份,且不同令牌不会因同后缀串味)。
1835
1854
  */
1836
1855
  async fetchBalance() {
1837
1856
  const { value: token } = await tokenStore.resolve();
1838
1857
  if (token === "") throw pluginError(CODE.AUTH_ERROR, "no ModelScope token configured — set MODELSCOPE_API_KEY or save one in the panel");
1839
1858
  const url = settings.siteBase + "/openapi/v1/magicubes/balance";
1840
- const key = "balance#" + token.slice(-6);
1859
+ const key = "balance#" + tokenFingerprint(token);
1841
1860
  return await read(key, async () => {
1842
1861
  const response = await fetchWithTimeout(url, {
1843
1862
  method: "GET",
@@ -2617,15 +2636,28 @@ function createFileDrawStore(options = {}) {
2617
2636
  //#region src/host/draw-jobs.ts
2618
2637
  /**
2619
2638
  * 建一张出图作业表。
2620
- * @param {DrawJobsOptions} [options] - 容量上限。
2639
+ * @param {DrawJobsOptions} [options] - 容量与保护窗参数。
2621
2640
  * @returns {DrawJobs} 作业表。
2622
2641
  */
2623
2642
  function createDrawJobs(options = {}) {
2624
2643
  const maxJobs = Math.max(1, Math.floor(options.maxJobs ?? 64));
2644
+ const resumeWindowMs = Math.max(1, Math.floor(options.resumeWindowMs ?? 9e5));
2645
+ const hardMaxJobs = Math.max(maxJobs, Math.floor(options.hardMaxJobs ?? maxJobs * 2));
2625
2646
  /** @type {Map<string, DrawJob>} 插入顺序即淘汰顺序(Map 保序)。 */
2626
2647
  const jobs = /* @__PURE__ */ new Map();
2627
- const evictIfNeeded = () => {
2628
- while (jobs.size > maxJobs) {
2648
+ /**
2649
+ * 超容时的两级淘汰(判据见文件头「生命周期」):
2650
+ * 软上限只淘汰已过续查保护窗的(越过总放弃线,续查只换来「超过总预算」);
2651
+ * 硬上限保底——全是保护窗内的作业也绝不无界。
2652
+ * @param {number} now - 当前墙钟毫秒(`create` 的 `now` 缝传进来,测试可造旧作业)。
2653
+ */
2654
+ const evictIfNeeded = (now) => {
2655
+ if (jobs.size <= maxJobs) return;
2656
+ for (const [id, job] of jobs) {
2657
+ if (jobs.size <= maxJobs) return;
2658
+ if (now - job.submittedAt >= resumeWindowMs) jobs.delete(id);
2659
+ }
2660
+ while (jobs.size > hardMaxJobs) {
2629
2661
  const oldest = jobs.keys().next();
2630
2662
  if (oldest.done === true) return;
2631
2663
  jobs.delete(oldest.value);
@@ -2634,18 +2666,19 @@ function createDrawJobs(options = {}) {
2634
2666
  return {
2635
2667
  create({ taskId, model, prompt, now }) {
2636
2668
  const jobId = taskId;
2669
+ const at = typeof now === "number" && Number.isFinite(now) ? now : Date.now();
2637
2670
  const job = {
2638
2671
  jobId,
2639
2672
  taskId,
2640
2673
  model,
2641
2674
  prompt,
2642
- submittedAt: typeof now === "number" && Number.isFinite(now) ? now : Date.now(),
2675
+ submittedAt: at,
2643
2676
  polls: 0
2644
2677
  };
2645
2678
  const previous = jobs.get(jobId);
2646
2679
  if (previous !== void 0) job.polls = previous.polls;
2647
2680
  jobs.set(jobId, job);
2648
- evictIfNeeded();
2681
+ evictIfNeeded(at);
2649
2682
  return { ...job };
2650
2683
  },
2651
2684
  get(jobId) {
@@ -3286,6 +3319,9 @@ function createDrawCooldown(cooldownMs) {
3286
3319
  * 轮询只与更慢相关。
3287
3320
  * 3. 失败原因实测可能**不可知**(`errors.code:0` + 空 message)。`describeDrawFailure`
3288
3321
  * 因此明说「未给出原因」,本文件也**不从空错误里编造**。
3322
+ * 4. 轮询的 5xx / 429 / 408 是**瞬态**不是终局({@link isTransientPollStatus}):
3323
+ * 同一个物理故障不因表现为 TCP 抛错还是 5xx 响应就两种判法;只有 4xx 是
3324
+ * 终局证据。瞬态持续时由 deadline 收口成 pending,续查把手不丢。
3289
3325
  *
3290
3326
  * @module dsh-connect-modelscope-token-plan/draw-runtime
3291
3327
  */
@@ -3393,8 +3429,10 @@ async function submitDrawTask({ fetchImpl, apiBase, apiKey, model, prompt, image
3393
3429
  * 抛错是「这件事做不成了」,`pending` 是「还没成,可以再查」。同一段循环因此
3394
3430
  * 同时服务于两种调用形状(一次性跑完 / 分段续查),不写两份。
3395
3431
  *
3396
- * 轮询带 `X-ModelScope-Task-Type`(**必需**,漏了必假失败);传输层抖动与非
3397
- * JSON 响应体都**不升级为终局**,继续等下一轮。
3432
+ * 轮询带 `X-ModelScope-Task-Type`(**必需**,漏了必假失败)。三类情形**不升级
3433
+ * 为终局**,在本次预算内继续等下一轮:① 传输层抛错(ECONNRESET / DNS);
3434
+ * ② 非 JSON / 200 垃圾响应体(CDN 错误页);③ **瞬态 HTTP 状态码**(5xx /
3435
+ * 429 / 408,见 {@link isTransientPollStatus})。只有 4xx 是终局证据。
3398
3436
  * @param {object} options - 等待参数。
3399
3437
  * @param {DrawFetch} options.fetchImpl - 注入的 fetch。
3400
3438
  * @param {string} options.apiBase - apiBase。
@@ -3453,6 +3491,7 @@ async function awaitDrawTask({ fetchImpl, apiBase, apiKey, taskId, waitMs = DRAW
3453
3491
  }
3454
3492
  const pollText = await safeText(polled);
3455
3493
  if (!polled.ok) {
3494
+ if (isTransientPollStatus(polled.status)) continue;
3456
3495
  const previous = lastStatus !== void 0 ? `(上一轮读到 ${lastStatus})` : "";
3457
3496
  return {
3458
3497
  kind: "failed",
@@ -3805,6 +3844,24 @@ function toPositiveNumber(value) {
3805
3844
  const n = Number(value);
3806
3845
  return Number.isFinite(n) && n > 0 ? n : void 0;
3807
3846
  }
3847
+ /**
3848
+ * 轮询响应里的**瞬态** HTTP 状态码:不判作业死刑,本次等待预算内继续等下一轮。
3849
+ *
3850
+ * 判据是「同一个物理故障不因表现形态而异」:传输层抛错(ECONNRESET / DNS
3851
+ * 解析失败)早就走的是「继续等下一轮」,而同一种 CDN 边缘抖动、上游短暂过载
3852
+ * 以 **5xx/429 响应**的形态出现时,旧写法直接判失败、作业出表、进 60s 冷却
3853
+ * ——agent 想续查只能重新提交一张图。上游排队窗口实测 1–15 分钟
3854
+ * (docs/SPIKE-image-gen.md),那个窗口内的边缘 5xx 是常态噪声,不是终局。
3855
+ *
3856
+ * 4xx 保持终局:401/403 = 令牌或权限真的没了,404/400 = 任务或端点真的没了
3857
+ * ——这些等下去也不会变。deadline 负责收口:瞬态一直持续时耗尽预算后返回
3858
+ * `pending`,两段式的续查把手(jobId)不丢。
3859
+ * @param {number} status - 轮询响应的 HTTP 状态码。
3860
+ * @returns {boolean} 是否按瞬态继续等。
3861
+ */
3862
+ function isTransientPollStatus(status) {
3863
+ return status === 408 || status === 429 || status >= 500;
3864
+ }
3808
3865
  /** 读 body 文本,读不到给空串(错误分诊不能因为读 body 失败而失败)。 */
3809
3866
  async function safeText(response) {
3810
3867
  try {
@@ -6173,7 +6230,7 @@ function apply(ctx, config = {}, deps = {}) {
6173
6230
  profile,
6174
6231
  logger: ctx.logger
6175
6232
  });
6176
- const drawJobs = createDrawJobs();
6233
+ const drawJobs = createDrawJobs({ resumeWindowMs: settings.drawTimeoutMs });
6177
6234
  const usagePrefsStore = createFileUsagePrefsStore({
6178
6235
  profile,
6179
6236
  logger: ctx.logger
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "ModelScope Model Access",
4
- "description": "Bring the ModelScope language and image-generation models into DSH as default-off switches, with image recognition and context windows filled in automatically. The card also shows the free Magicube balance, links each model to its ModelScope page, and records local call counts, a daily trend and rate-limit events. All of it needs only an API key, with no account sign-in."
4
+ "description": "One-tap connect ModelScope language and image-generation models into DSH (off by default), with image recognition and context window filled in automatically. You can also see the free Magicube balance, jump to a model's info page, and record local call counts, trends and rate-limit events, with a usage pill beside the input box showing the balance and today's local count. All of it needs only an API key, with no account sign-in."
5
5
  }
6
6
  }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "魔搭模型接入",
4
- "description": "一键接入魔搭社区的语言、生图模型到 DSH ,自动填入图片识别与上下文窗口(默认关);还可查看免费魔粒余额、跳转到模型信息页,记录本地调用计数、趋势、限流事件。上述功能仅需填入 API key ,不需要登陆账号。"
4
+ "description": "一键接入魔搭社区的语言、生图模型到 DSH(默认关),自动填入图片识别与上下文窗口;还可查看免费魔粒余额、跳转到模型信息页,记录本地调用计数、趋势、限流事件,输入框旁另有用量胶囊显示余额与今日本地调用次数。上述功能仅需填入 API key,不需要登录账号。"
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -2,13 +2,13 @@
2
2
  "name": "dsh-connect-modelscope-token-plan",
3
3
  "displayName": "DSH Connect ModelScope Token Plan",
4
4
  "icon": "./icon.svg",
5
- "version": "0.6.3",
5
+ "version": "0.6.4",
6
6
  "homepage": "https://github.com/eghrhegpe/dsh-connect-modelscope-token-plan",
7
7
  "bugs": {
8
8
  "url": "https://github.com/eghrhegpe/dsh-connect-modelscope-token-plan/issues"
9
9
  },
10
10
  "author": "eghrhegpe",
11
- "description": "Bring the ModelScope language and image-generation models into DSH as default-off switches, with image recognition and context windows filled in automatically. The card also shows the free Magicube balance, links each model to its ModelScope page, and records local call counts, a daily trend and rate-limit events. All of it needs only an API key, with no account sign-in.",
11
+ "description": "One-tap connect ModelScope language and image-generation models into DSH (off by default), with image recognition and context window filled in automatically. You can also see the free Magicube balance, jump to a model's info page, and record local call counts, trends and rate-limit events, with a usage pill beside the input box showing the balance and today's local count. All of it needs only an API key, with no account sign-in.",
12
12
  "keywords": [
13
13
  "dsh-plugin",
14
14
  "deepseek-harness",
package/screenshots.json CHANGED
@@ -1,5 +1,6 @@
1
1
  [
2
2
  "assets/tab-account-and-usage.png",
3
3
  "assets/tab-model-list.png",
4
- "assets/tab-model-draw.png"
4
+ "assets/tab-model-draw.png",
5
+ "assets/modelscope-usage-capsule.png"
5
6
  ]