dsh-connect-modelscope-token-plan 0.1.0 → 0.2.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
@@ -1,166 +1,33 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
4
-
5
- 两轮:先是发布前的漂移收口与门禁补强,再是一轮由双路深审挖出的**数据安全与红线修复**。
6
-
7
- ### 第五轮:红线收口
8
-
9
- - **聚合层 `soft()` 补脱敏**(红线 1 的收口漏洞):`soft()` 用裸 `errMsg(error)`,而那个
10
- 字符串会一路流进 `shapeWarnings` / `balance.error` / `models.error`(面板可见)与快照
11
- 路由的响应体。它隐式依赖「下游每个 promise 都已自行脱敏」——**契约不该靠下游的自觉**。
12
- 实测 `errMsg` 会把 `upstream said ms-3f2a1b8c1111222233334444555566` 原样带出去。现在收口
13
- 处自己脱敏(`redactSecrets` 是幂等的,重复脱敏换掉「凭据可能外泄」这个尾部风险划算)。
14
- - **另两处同类出口**:`resolveSettings` 的 `configError`(进 `ok:false` 响应体)与 `index.ts`
15
- 的路由注销警告(**进日志**——红线 1 同时管日志与面板)。都补上脱敏。
16
- - **`test/credentials.test.mjs` 加一条静态不变量**:Host 侧不允许出现「裸 `errMsg` 进入
17
- wire 字段或日志」,且点名三个具体收口。判据是静态的——构造真实的泄漏路径需要上游回显,
18
- 而离线套件刻意不联网。已双向验证(去掉任一脱敏,对应用例变红)。
19
-
20
- ### 第四轮:资源与一致性
21
-
22
- - **原子写补崩溃一致性与临时文件清理**(`writeStateFile`,四个 store 共同的唯一写入口):
23
- - `rename` 失败时(Windows 上目标文件被占用最常见)临时文件**留在原地**——名字是
24
- `<base>.<pid>.<ts>.<uuid>.tmp`,全仓原本无任何 `unlink`,于是
25
- `state/<profile>/<name>/` 会慢慢堆满垃圾。现在失败时删掉自己的临时文件再重抛
26
- (清理失败不掩盖原始错误)。
27
- - rename 前对临时文件 `fsync`。这是**崩溃一致性**(rename 本身只保证并发原子性):
28
- 没有它,断电/内核崩溃时 rename 可能把一个空或半截文件立到目标路径,而
29
- `parsePayload` 对损坏文件是「读作未设置」**静默降级**——用户看到「清单和历史全没了」
30
- 而没有任何错误。代价是真机每次几毫秒,值得:`usage.json` 每次对话都写、
31
- `provider.json` 每次点开关都写,都不是低频操作。
32
- - **修未消费的 response body 占住连接**:`fetchTaskTags` 在 `!response.ok` 分支直接
33
- `return []`、不读也不取消 body。undici 里未消费的 body 会**占住连接直到 GC**,而目录
34
- 刷新对 35 个模型并发打详情端点——任何一条走该分支,连接就一直挂着,连接池被吃满后
35
- 后续请求开始排队。`readErrorMessage` 的 catch 分支同理(`json()` 抛在半路时 body 未被
36
- 消费)。现在两条路径都显式 `void response.body?.cancel()`。
37
- - **删 27 个零引用样式token**(83 → 56):`poolsGrid` / `quotas` / `quotaRemaining` /
38
- `statHeadline` / `details*` / `primaryHover` / `primaryBusy` 等。它们连同解释其设计
39
- 决策的注释一起是「上一轮删模型目录表 / 次数口径推算条」的**化石**——注释里还在描述
40
- 「额度双栏」「grant chip」「details 折叠区」「twin card」,读代码的人会以为这些 UI 还在。
41
- 已连带清理 5 处引用已删 token 的注释。
42
- - **删 4 个死 helper**:`when()`(注释声称「头部更新于必须能熬过隔天」,而 `panel-page`
43
- 实际用 `isoTime`,该设计从未落地)、`postJsonOrThrow`、`dictKey`,以及
44
- `sortCatalogIds`(featured 置顶排序随「模型目录表」退役,`roster` 现按 Host 返回顺序
45
- 渲染,只保留 featured 徽标——排序规则连同它的 6 条性质断言一起删,不留「有契约无
46
- 实现」的孤儿)。`clockLong` 经核实**有 2 处真实调用**(`format.ts:24,42`),保留。
47
- - **新增 `test/state-store.test.mjs`**:`state-store.ts` 是四个 store 共同的唯一写入口,
48
- 此前零直接测试。
49
-
50
- ### 第三轮:状态机与配置边界
51
-
52
- 派代理专门核实三个「需要真peer 才能判定」的疑点——它在DSH 发行版里找到了
53
- `@deepseek-ai/dsh-llm@0.1.7-rc.2` 的**本机真实源码**,把三个疑点从推断升级为读代码判定。
54
-
55
- - **修 dispose × 在途 publish 竞态(僵尸 provider 泄漏)**:`publishProviderOnce` 只在
56
- **入口**查一次 `queue.isDisposed()`,而注册发生在最后一个 await 之后(动态 import
57
- 是最宽的窗口)。teardown 落在这段窗口里时闸门不重跑,于是 `registerAdapter` 把适配器
58
- 注册进一个**已撤下本插件的 Host**,而 teardown 早已跑完、release 永不调用(全仓只有
59
- 一个 `.release()` 调用点)。泄漏的对持有 `tokenStore` 与整个 Cordis `ctx` 的闭包 →
60
- **僵尸 provider**:picker 里那个 provider 还在、还能路由,而面板路由已注销,用户没有
61
- 界面能关掉它。
62
- peer 源码证实 fiber 兜底救不了:`registerAdapter` 用的是 **llm 服务自己的**
63
- `ctx.effect`(`.bind(this)` 到 llm 服务),插件卸载不会 dispose 它。
64
- 修法:在 `swapRegistration` 前补第二次闸门检查——从那里到 `registerAdapter` 之间
65
- **没有任何 await**,单线程下无法插入 teardown,两种时序都正确。
66
- - **给 provider-store 加写串行化(并发丢数据)**:这个 store 曾**完全没有写串行化**
67
- (`usage-store` 有 `writeChain`,这里没有),而 `patchPayload` 是读-改-写。面板开关
68
- POST 与 roster POST 是两个独立请求,各自读到同一个 `current`,后写的赢——实测用户勾了
69
- 模型又开了开关,两次点击只活下来一次。文件头说「两个字段必须作为一个载荷读写」防住了
70
- 「不同字段互相覆盖」,但缺串行化后「两次都基于同一快照」的互相覆盖照样发生。现在整段
71
- 读-改-写排队,`writePayload` 的版本拒绝语义一起搬进临界区。**legacy 继承回填**(读路径
72
- 内触发的写)也走同一条链,否则它会覆盖用户刚保存的清单,且因只发生一次而永久生效。
73
- - **给三个时长配置补上界**:`cacheSeconds` / `pollSeconds` / `inferenceTimeoutMs` 的
74
- `clampInt` 只传了 min(max 默认 `Infinity`)。`pollSeconds × 1000` 超过 2^31-1 ms 时
75
- Node 把 `setInterval` 延时**钳到 1ms**(实测 pollSeconds=3000000 → `_idleTimeout=1`),
76
- 插件变成每毫秒打一次魔搭目录并打穿单飞缓存;`inferenceTimeoutMs` 同理让每次请求立刻超时。
77
- 现在按语义夹到轮询 1 小时 / 缓存 10 分钟 / 超时 10 分钟。
78
- - **新增 `test/provider-publish.test.mjs`**:`provider-publish.ts` 此前**零测试覆盖**
79
- (ROADMAP 声称 `test/provider.test.mjs` 测「publish 三条语义」,实际上没有)——零覆盖
80
- 正是这个 P0 能藏住的原因。新套件覆盖 dispose 闸门(含 await 窗口竞态)、publish 队列
81
- 串行性、回滚恢复旧对、空转守卫(指标是 `registerAdapter` 次数而非工厂次数——守卫在工厂
82
- 之后,被跳过的 publish 也跑过工厂)、签名的确定性。
83
- - **核实并排除两条疑点**(避免误改):`{...handle}` 展开丢私有字段**不是问题**——peer
84
- 的 `prepareCall` 返回 plain object literal(源码 `:1827-1833`),无原型方法无私有字段;
85
- `resolveRegistrationService` 漏清 `state.built` **不构成 bug**——重新注册旧适配器是安全的
86
- (无 DUPLICATE_ADAPTER、A 上无注册域状态、新 release 有记录),只是语义不一致。
87
-
88
- ### 第二轮:深审挖出的真问题(每条都有复现证据,不是推断)
89
-
90
- - **修 usage-store 版本守卫(数据静默销毁)**:守卫检查写在 `enqueue` 开头,而
91
- `readOnly` 的置位发生在其后的 `cache.read()` 内部——判据比真正的探测**晚了一
92
- 步**。实测:磁盘预置 version 2 含 110 次真实调用,一次 `recordCall` 后 version
93
- 降级成 1、历史归零,而日志还在说「recording disabled to avoid clobbering」——
94
- 旧构建正在降级覆写新构建的数据,日志与磁盘状态互相撒谎。同一处 `readOnly` 只
95
- 置位不复位,于是用户按 doctor 建议删掉 usage.json 后写入永久静默丢弃、面板
96
- 还在显示内存残留。现在版本探测由写路径自己做(判据与写入之间不再隔 await),
97
- 且「版本已知」时复位闩锁。回归用例见 `test/usage-store.test.mjs` §7b/§7c(已验证
98
- 它们在修复前确实变红)。
99
- - **修信任围栏的前缀匹配**:`hostName` 对 `Host: [::1]evil.com` 读成 `[::1]` 并
100
- **通过**白名单——白名单是精确集合,前缀匹配等于把它放宽成「以成员开头」。现在
101
- 校验 `]` 之后只允许空或 `:port`。
102
- - **修面板缺键即白屏**:`interpretSnapshot` 对 `ok:true` 的 body 是裸 cast,
103
- `SNAPSHOT_REQUIRED_KEYS` 只在测试里被引用、运行时零防线。缺 `models` / `trend` /
104
- `token` 任一 → TypeError炸掉整个面板(那一行在**所有 tab 上都执行**),而只有
105
- `provider` 有防御。现在按 `SNAPSHOT_REQUIRED_KEYS` 校验并把缺失块补成与 Host
106
- 降级同构的空形状,缺失名单作为 `missingKeys` 冒到面板上(不静默)。顺带收口同
107
- 族裸解引用(`data?.models.sample` 的 `?.` 没到底、`TokenForm` 的 `=== null` 漏
108
- `undefined`、`LocalDailyCard` 裸解构)。
109
- - **修 429 分诊把限频误判成额度耗尽**:`classifyRateLimit` 曾把 `"limit reached"` /
110
- `"daily"` / `"次数"` 归进 quota 桶,于是「Rate limit reached, retry after 60s」
111
- 被判成耗尽 → `llm-retry` 刻意排除 QUOTA → **瞬时限频快失败且不重试**;面板事件
112
- 流还把 rpm 限频显示成「额度耗尽」,与 `llm-error-fix` 的纠正层自相矛盾。现在判据
113
- **复用** `llm-error-fix` 的 `looksLikeRateLimit` + `hasHardQuotaWordingIn`(消除
114
- 第二份词表),并补上 `daily|monthly … limit` 这组硬额度措辞。
115
- - **修 `internal_error` 没有引导**:聚合器兜底码曾不在 `CODE` 表里,于是 Client 的
116
- `GUIDANCE_BY_CODE` 映射不了它,最需要人看的内部错误反而没有引导文案,且因为不
117
- 在 `FORM_EXCLUDED_CODES` 里被引导去「配令牌」——方向反了。现在
118
- `CODE.INTERNAL_ERROR` 正式进表,两端同一份真源。
119
- - **修令牌脱敏漏紧凑写法(红线 1)**:`redactSecrets` 的 `ms-` 正则硬编码 UUID 的
120
- `8-4-4-4-12` 分组形状,紧凑写法 `ms-3f2a1b8c1111222233334444555566` 整条漏网;
121
- 而 `ms-auth.save()` 不做形状校验,非标准形状的令牌是**可达状态**,上游一旦回显
122
- 即进日志与面板响应。两头都堵:脱敏闸放宽到 `ms-[0-9a-f-]{16,}`,入口新增形状
123
- 校验拒收遮不住的短值。
124
- - **修 `forget()` 谎报删除成功(红线 1)**:曾吞掉凭据服务的 `unset` 失败,让路由回
125
- `ok: true`,而令牌**仍在凭据文件里且仍被读回来**。安全动作谎报成功比失败更糟,
126
- 现在失败向上传播(路由已有脱敏呈现路径)。
127
- - **修 `state().ephemeral` 答错问题**:wire 定义是「这个值重启会不会丢」,实现却答
128
- 「有没有凭据服务」——于是 env 来源的值(重启不丢)被标成 ephemeral。现在按来源
129
- 答:只有 `memory` 才真 ephemeral。
130
- - **修 `parseRetryAfterMs` 漏英文单位**:只认中文单位,上游文案换成
131
- `retry after 5 seconds` 就静默退化成「无时长」。补英文单位时还抓到一个**新引入的
132
- 30 倍退避差**——单位写成了非捕获组 `(?:...)`,`en[2]` 恒为 undefined,`2 minutes`
133
- 会被当成 2 秒。
134
- - **修 HTML 响应被塞进面板**:`use-snapshot-polling` 的裸 `await response.json()`
135
- 在反向代理/登录墙回 HTML 时抛 SyntaxError,而 `errorText` 对 Error 返回
136
- `.message`,于是那段 HTML 源码会显示在面板正文(同仓 `http.ts` 早有 `.catch`,
137
- 这里是漏)。
138
- - 新增两个套件:`test/codes.test.mjs`(`classifyRateLimit` 此前**零测试覆盖**,所以
139
- 它能悄悄漂移)、`test/credentials.test.mjs`(红线 1 的专属门禁,此前也没有)。
140
- 后者逐档比对入口与脱敏闸**同宽**——这是防「进得来却遮不住」的关键不变量。
141
-
142
- ### 第一轮:漂移收口与门禁补强
143
-
144
- - **修配置面漂移(会误导用户的那一处)**:`cordis.patch.yml` 仍写着「本地计数只覆盖
145
- probe、不覆盖 provider 接入后的对话推理调用」——该说法在 `usage-observer` 落地时就
146
- 已作废,而这份文件正是操作者改配置时唯一在手边的文档。改为准确描述:计数经流出口
147
- 观察器覆盖**全部 DSH 魔搭调用**,并写明已知偏差方向是**少记**(peer 的重试在同一条
148
- 流内部重发)。
149
- - **新增产物新鲜度门禁 `test/build-gate.mjs`**:`lib/` 与根 `client.js` 是故意入库的
150
- 产物(`github:` 安装源不跑 prepack),此前 `.gitignore` 里「后续接入」的 build-gate
151
- 并不存在——**改了`src/` 忘build,市场装到旧代码,而工作树干净、测试与typecheck 全绿,
152
- 没有任何东西会红**。判据是**内容哈希不是 mtime**(clone / 解压都会重写时间戳):重新
153
- 构建到系统临时目录,与入库产物逐字节比对,且**只读工作树**。已挂进 `npm test` 尾部。
154
- - **删死配置 `dailyQuotaTotal` / `dailyQuotaPerModel`**:面板早已移除次数口径的推算条,
155
- 「M4 阈值提醒预留」的前提(M4)已落地且阈值提醒不存在。客户端对两者零消费,故连同
156
- wire 的 `quota.daily.limit` / `remainingComputed` / `quota.perModelLimit` 一并收窄为
157
- `quota.daily.usedLocal` 单字段。`test/config.test.mjs` 显式钉死「它们不回来」。
158
- - **清悬空引用**:源码里 20+ 处引用本仓库不存在的 `PITFALLS §NN` / `docs/IMPROVEMENTS.md`、
159
- 以及一个本仓库没有的 Raccoon 插件的 bug 史。**保留知识、删掉死指针**——坑是什么仍写
160
- 在注释里,只是不再指向查不到的编号。
161
- - **新增 CI**(`.github/workflows/gate.yml`):typecheck + 20 套件离线门禁。CI 是唯一能
162
- 抓到「提交了忘 build」的地方。`on` 加了引号(裸 `on` 被 YAML 1.1 解析成布尔 true,
163
- 工作流不报错但永不触发)。
3
+ ## [0.2.0] — 2026-10-05
4
+
5
+ **凭据与红线**
6
+ - 上游报错原文经 `soft()` 直进面板与快照(实测 `errMsg` 把 `upstream said ms-…` 原样带出);聚合层与 `configError`、注销警告两处出口统一走 `redactSecrets`。
7
+ - `ms-` 脱敏只认 UUID 分组,紧凑 32 位写法整条漏网;放宽到 `ms-[0-9a-f-]{16,}`,保存入口同步拒收遮不住的短值。
8
+ - `forget()` 吞掉凭据服务失败仍回 `ok:true`,谎报删除成功;现在向上传播。
9
+ - 面板 `token.hint` 把环境变量说成「回退」——分层正好相反:env 在 DSH 启动时读一次、优先级高于面板保存值。README 与 `cordis.patch.yml` 的同源假话一并更正。
10
+
11
+ **数据不再悄悄没了**
12
+ - usage-store 版本守卫的判据晚于探测一步:旧构建会用 version 1 降级覆写 version 2 文件(实测 110 条历史归零);`readOnly` 只置位不复位,删文件后永久静默丢弃。
13
+ - provider-store 无写串行化:两次点击只活下来一次;读-改-写整体排队,legacy 回填同链。
14
+ - `writeStateFile` rename 前补 `fsync`(崩溃后残缺 JSON 会被静默读成「没数据」);rename 失败时清理临时文件;两处未取消的 body 占住连接。
15
+
16
+ **可用性**
17
+ - 快照缺 `models` / `trend` / `token` 任一键即 TypeError 白屏整页;按必需键校验、缺失补空形状,并把缺失名单显式冒出来。
18
+ - 429 分诊把 rpm 限频误判成「额度耗尽」——恰好是刻意不重试的那条路;判据改为复用 `llm-error-fix` 词表。
19
+ - `parseRetryAfterMs` 只认中文单位,补英文时又抓到一处 30 倍退避差(`2 minutes` 被当 2 秒)。
20
+ - `pollSeconds` 等三项无上界:填 3000000 时 `setInterval` 钳到 **1ms**,每毫秒打一次上游;按语义封顶。
21
+ - HTML 响应体不再被塞进面板正文;`internal_error` 有引导文案;`state().ephemeral` 按来源答。
22
+ - dispose × 在途 publish 竞态造出「僵尸 provider」——picker 里还能路由、面板已注销;`swapRegistration` 前补第二次闸门检查。
23
+
24
+ **移除**
25
+ - probe 的 `kind:"usage"` 分支:缺省即计费调用、裸 POST 真花魔粒,客户端早已发不出、服务端仍在。
26
+ - `dailyQuotaTotal` / `dailyQuotaPerModel`(面板不消费,wire 收窄为 `quota.daily.usedLocal`);`maxEvents` 自创建起从未进文档与测试,本轮补登记。
27
+
28
+ **门禁**:新增产物新鲜度(内容哈希对比,红得对)、文档一致性(凭据分层 / 已回填预告 / 递归子目录清单)与 state-store、provider-publish、codes、credentials、retry、route-shape 套件;`.github/workflows/gate.yml` 首次落地;变异测试工具自己的两个缺陷修复(此前所有审计报告不可信)。
29
+
30
+ > **实施过程(病灶、取证、判据)全部在 [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md)**——那几轮里不少是踩过的坑,照着做能绕开。
164
31
 
165
32
  ## [0.1.0] — 2026-10-05
166
33
 
package/README.md CHANGED
@@ -22,7 +22,9 @@
22
22
 
23
23
  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))。推理响应本身仍不带任何额度头。
24
24
  2. **本地计数是辅助口径**:面板头条是官方魔粒余额;本地计数只回答官方余额答不了的问题——按模型分布、本地趋势、429 事件流(只统计经本插件的调用,直连魔搭的其它客户端不计入)。次数口径的「推算剩余/参考上限」已从面板移除(官方改魔粒计费后两个单位并排是误导),相应的 `dailyQuotaTotal` / `dailyQuotaPerModel` 两个社区快照常数也从配置里一并删除了——面板不消费它们,留着只会让人以为能算「还剩几次」。
25
- 3. **凭据只有一把静态钥匙**:魔搭访问令牌(个人中心生成,形如 `ms-…`),无 OIDC、无密码、无 refresh。默认读 DSH 凭据服务已有的 `MODELSCOPE_API_KEY` 引用,`MODELSCOPE_API_KEY` 环境变量作回退;令牌永不入日志、永不进插件目录。
25
+ 3. **凭据只有一把静态钥匙**:魔搭访问令牌(个人中心生成,形如 `ms-…`),无 OIDC、无密码、无 refresh。默认读 DSH 凭据服务已有的 `MODELSCOPE_API_KEY` 引用;`MODELSCOPE_API_KEY` 环境变量只在启动时读一次,且优先级高于存储值(坑见下段);令牌永不入日志、永不进插件目录。
26
+
27
+ **凭据分层有个坑(2026-10-05 实测)**:DSH 凭据服务的优先级里,**启动时的环境快照压过存储文件**,而那个快照**在启动时冻结**(不是活的 `process.env`)。两个后果:①在 DSH 启动**之后**改环境变量——包括 Windows 用户级变量——**不会被看见**,改完必须**完全重启 DSH**(退出全部 `DeepSeek Harness` 进程,不只是关窗口)才进快照;②重启让环境变量生效后,它会**反过来压过面板里保存的值**,而面板仍显示「已保存」。**最省事的做法是直接在面板里保存令牌**(立即生效,且不依赖重启)。
26
28
 
27
29
  ## 免费额度怎么算(2026-10 快照,以官方为准)
28
30
 
@@ -31,6 +33,9 @@
31
33
  - 额度按天重置;耗尽返回 429。
32
34
  - `/v1/models` 免认证可读,**不消耗额度**。
33
35
 
36
+ 上面这些数字来自社区公开信息,非官方数据,官方调整后需手动更新本节;**它们只用于解释
37
+ 「额度怎么算」,不参与任何计算**——面板不消费次数上限,「还剩几次」算不出来(见下节)。
38
+
34
39
  ## 文档地图
35
40
 
36
41
  | 何时 | 查 |
@@ -40,16 +45,16 @@
40
45
  | **这版是怎么做出来的、踩过什么坑** | [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md)(实施过程档案) |
41
46
  | 上游端点全景 / 第三方方案对比 | [docs/REFERENCES.md](docs/REFERENCES.md)、[docs/REFERENCE-modelsdev.md](docs/REFERENCE-modelsdev.md) |
42
47
  | v1 要做什么、做到哪了 | [docs/ROADMAP.md](docs/ROADMAP.md) |
43
- | Host/Client 两半怎么分 | sensenova 仓库 `docs/ARCHITECTURE.md`(本仓库按同构落地后再补自己的) |
48
+ | Host/Client 两半怎么分 | [docs/PROVIDER-M4.md](docs/PROVIDER-M4.md)(§14 红线)+ [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md);本仓库暂无独立的 `ARCHITECTURE.md` |
49
+ | 开发者校验工具(变异测试 / 覆盖率) | [tools/dev/README.md](tools/dev/README.md);发布流程见 [RELEASING.md](RELEASING.md) |
44
50
 
45
51
  ## 诚实声明
46
52
 
47
53
  - 面板头条是**官方魔粒余额**(真实值);本地计数只统计**经本插件的调用**——它回答的是分布/趋势/429 事件流,不是余额,直连魔搭的其它客户端不计入。注册 provider 之后,DSH 的全部魔搭调用都经本插件,所以本地计数**覆盖全部 DSH 魔搭调用**。次数口径的「推算剩余」已从面板移除(官方改魔粒计费后两单位并排是误导),相应地`quota.daily` 只剩 `usedLocal` 一个数——没有 `limit`,也没有推算出的「剩余」。
48
54
  - **本地计数的口径与 harness 全局账本不同,且刻意不合并**。DSH 自己有 `@linxin666/dsh-usage`,按 `days→provider→model` 记成功调用的 token;它只订阅 session 的 `assistant/message`,**失败调用一次都不记**。所以本插件只做两件全局账本答不了的事:①「经本插件的调用分布与趋势」;②**429/错误事件流**——这是本插件的独家数据(官方只给总额度余额,不给「哪个模型刚才在限频」)。成功的 token 总量不在此重复记账(详见 `src/host/usage-observer.ts` 文件头)。
49
55
  - 429/错误事件的分类读的是 **`llm-error-fix` 重分类之后**的 code:被误判成 QUOTA 的 rpm 限频在事件流里记作 `rate_limit`,与退避策略说同一件事。观察层套在重分类层**之外**,顺序不可反。
50
- - **计数口径**:每次流结束记一次 call,**含失败、含用户中断**(魔搭按次数计费,限频掉的请求同样是一次调用);token 数上游没给时是 `null` 而非 0。已知偏差方向是**少记**:peer 的重试在同一条流内部重发,观察器只看到一条流——宁可少记也不虚增。
56
+ - **计数口径**:每次流结束记一次 call,**含失败、含用户中断**(魔搭按次数计费,限频掉的请求同样是一次调用);token 数上游没给时是 `null` 而非 0。偏差方向是**按流计次**:重试不在流内部发生,而是在流消费完后由 `agent/request-error` 瀑布决定重发,因而**每次重试各记一次**——按「魔搭按次数计费」的口径这反而是对的(每次重发都真打了一次上游)。
51
57
  - **provider 默认关**(opt-in,与姊妹插件一致):面板「模型」tab 的「接入为 DSH 模型」开关翻转即生效,面板保存值优先于 `cordis.patch.yml` 的 `registerProvider` 默认。
52
- - **已知限制:`reasoning` 恒为 false**。魔搭 API-Inference 是多模型代理,是否吃 `reasoning_effort` 取决于背后那个模型;本插件无法离线得知每个 id 的档位表,保守默认不发该参数(模型用自己的默认),也不提供思考强度选择器。「宁可不选,不可错发」——发错档位会整条请求 400。见 [docs/PROVIDER-M4.md](docs/PROVIDER-M4.md) §4。
53
- - 额度常数来自社区公开信息,非官方数据;官方调整后需要手动更新配置。
58
+ - **已知限制:`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。
54
59
  - 429 响应形状未实测(不值得烧额度去触发),解析按 OpenAI 惯例兼容,漂移时面板会给 `shapeWarnings`。
55
60
  - 面板「模型」tab 的每个模型都外链到其魔搭详情页(`https://www.modelscope.cn/models/{owner}/{model}`):魔粒单价只在网页展示、不在官方 API,所以这只是一个**人工核对用的跳转,不是数据源**(「为什么没有按模型消耗 API」见 [docs/REFERENCES.md](docs/REFERENCES.md))。
package/client.js CHANGED
@@ -120,7 +120,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
120
120
  "token.save": "保存",
121
121
  "token.forget": "忘掉已保存",
122
122
  "token.placeholder": "粘贴 ms-… 访问令牌",
123
- "token.hint": "在魔搭「访问令牌」页生成;面板默认读取凭据服务中的 MODELSCOPE_API_KEY,环境变量为回退。",
123
+ "token.hint": "在魔搭「访问令牌」页生成。保存即生效(写入 DSH 凭据服务,不依赖重启)。环境变量 MODELSCOPE_API_KEY 只在 DSH 启动时读一次,且优先级高于此处保存的值——设了它就别再用面板存。",
124
124
  "token.ephemeral": "此 Host 没有凭据服务:面板保存的令牌重启即丢,请改用凭据服务或环境变量。",
125
125
  "token.link": "打开魔搭访问令牌页 →",
126
126
  "source.credentials": "凭据服务",
@@ -204,7 +204,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
204
204
  "token.save": "Save",
205
205
  "token.forget": "Forget saved",
206
206
  "token.placeholder": "Paste an ms-… access token",
207
- "token.hint": "Generate one on the ModelScope access-token page; the panel reads MODELSCOPE_API_KEY from the credentials service first, environment as fallback.",
207
+ "token.hint": "Generate one on the ModelScope access-token page. Saving takes effect immediately (written to the DSH credentials service, no restart needed). The MODELSCOPE_API_KEY environment variable is read once at DSH launch and ranks ABOVE a value saved here — set it and stop using the panel.",
208
208
  "token.ephemeral": "This Host has no credentials service: a token saved in the panel is lost on restart. Use the credentials service or an environment variable.",
209
209
  "token.link": "Open the ModelScope access-token page →",
210
210
  "source.credentials": "credentials",
@@ -1544,10 +1544,7 @@ var dsh_connect_modelscope_token_plan_client = (function() {
1544
1544
  setVerifyNote(null);
1545
1545
  setVerifyError(null);
1546
1546
  try {
1547
- const body = await postJson(PROBE_PATH, {
1548
- modelId,
1549
- kind: "validity"
1550
- });
1547
+ const body = await postJson(PROBE_PATH, { modelId });
1551
1548
  if (body !== null && body.ok === true) setVerifyNote(format(tt("probe.validOk"), { status: Number(body.status ?? 0) }));
1552
1549
  else setVerifyError(typeof body?.error === "string" ? body.error : "HTTP error");
1553
1550
  } catch (reason) {
package/cordis.patch.yml CHANGED
@@ -4,9 +4,10 @@
4
4
  # 并可选把魔搭注册成 OpenAI 兼容 LLM provider。以下全是普通配置:在这里改
5
5
  # (或在 profile 的 cordis.patch.yml 里覆盖同 id 字段)后重装/重载生效。
6
6
  #
7
- # 魔搭访问令牌(个人中心「访问令牌」,形如 ms-…)不在这里配置:插件默认读
8
- # DSH 凭据服务里的 MODELSCOPE_API_KEY 引用(本机已存在),env
9
- # MODELSCOPE_API_KEY 是回退。见 docs/SPIKE.md 与 README.md。
7
+ # 魔搭访问令牌(个人中心「访问令牌」,形如 ms-…)不在这里配置:插件读 DSH
8
+ # 凭据服务里的 MODELSCOPE_API_KEY 引用(面板保存即生效)。环境变量
9
+ # MODELSCOPE_API_KEY 只在启动时读一次、且优先级高于引用值——设了它就别再用面板存
10
+ # (见 README「凭据分层有个坑」与 ms-auth.ts 文件头)。见 docs/SPIKE.md 与 README.md。
10
11
  - insert:
11
12
  - id: dsh-connect-modelscope-token-plan
12
13
  name: dsh-connect-modelscope-token-plan
@@ -25,6 +26,8 @@
25
26
  # ------------------------------------------------------------------
26
27
  # 本地趋势保留天数(插件私有状态文件里的天桶数量,与上游无关)。
27
28
  trendDays: 14
29
+ # 429/错误事件环形缓冲的上限(面板只展示最近的)。上界 1000,硬夹。
30
+ maxEvents: 50
28
31
  # Host 侧对 /v1/models 的缓存秒数;面板脚注直接引用此值。上界 600。
29
32
  cacheSeconds: 60
30
33
  # 面板轮询间隔,由 Host 下发、面板跟随。上界 3600(1 小时)。
@@ -46,8 +49,10 @@
46
49
  # 魔搭调用**,不只反映面板自身的探针。直连魔搭的其它客户端仍不计入
47
50
  # (本插件看不见它们),消耗总量以官方魔粒余额为准。
48
51
  #
49
- # 已知偏差方向是**少记**:peer 的重试在同一条流内部重发,观察器只看到
50
- # 一条流。详见 src/host/usage-observer.ts 文件头与 README「诚实声明」。
52
+ # 偏差方向是**按流计次**:重试不在流内部发生,而是流被消费完后由
53
+ # agent/request-error 瀑布决定重发,因而每次重试各记一次(按「魔搭按
54
+ # 次数计费」的口径这反而是对的)。详见 src/host/usage-observer.ts 文件头
55
+ # 与 README「诚实声明」。
51
56
  registerProvider: false
52
57
  # 追加可信 Host 名(默认 localhost / 127.0.0.1 / ::1,只增不替)。
53
58
  # allowedHosts: []
@@ -102,7 +102,7 @@ const CODE = Object.freeze({
102
102
  * `QUOTA_EXCEEDED_CODE`),列在此处纯为协议对齐与防御;本层不纠正它。
103
103
  */
104
104
  ACCOUNT_QUOTA: "ACCOUNT_QUOTA",
105
- /** 瞬时限频(peer retryPolicy 默认重试,本层把误判体拉回这里)。 */
105
+ /** 瞬时限频:本层把误判体拉回这里,由本 provider 自己的显式 retryPolicy 退避重试。 */
106
106
  RATE_LIMIT: "RATE_LIMIT"
107
107
  });
108
108
  /** 纯文本层面的限频信号(v1 启发用)。 */
@@ -744,7 +744,7 @@ const inject = ["webServer"];
744
744
  * 产物反而引入第二份真源;这里用常量、由 test/config.test.mjs 钉住与
745
745
  * package.json 一致。
746
746
  */
747
- const PLUGIN_VERSION = "0.1.0";
747
+ const PLUGIN_VERSION = "0.2.0";
748
748
  /**
749
749
  * 默认值。刻意不含额度常数——理由见本文件头。
750
750
  * 面板头条是官方魔粒余额,次数口径的推算已在面板收敛时删除,这里若留着
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as redactSecrets, S as pluginError, T as verbatim, a as resolveSettings, b as obj, c as LLM_PROVIDER_ID, d as normalizeEntry, f as resolveAllowedList, g as looksLikeRateLimit, h as hasHardQuotaWordingIn, i as name, n as inject, p as rosterWithAvailability, r as isAdmitted, s as LLM_DISPLAY_NAME, t as PLUGIN_VERSION, u as isVisionModel, v as degrade, w as str, x as optional, y as errMsg } from "./host-config-DBCiZOBW.js";
1
+ import { C as redactSecrets, S as pluginError, T as verbatim, a as resolveSettings, b as obj, c as LLM_PROVIDER_ID, d as normalizeEntry, f as resolveAllowedList, g as looksLikeRateLimit, h as hasHardQuotaWordingIn, i as name, n as inject, p as rosterWithAvailability, r as isAdmitted, s as LLM_DISPLAY_NAME, t as PLUGIN_VERSION, u as isVisionModel, v as degrade, w as str, x as optional, y as errMsg } from "./host-config-C4bWNPWU.js";
2
2
  import { join } from "node:path";
3
3
  import { mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
4
4
  import { randomUUID } from "node:crypto";
@@ -22,9 +22,22 @@ import { homedir } from "node:os";
22
22
  * 读取优先级,每次 resolve 都重走一遍:凭据服务(面板保存的值必须立即生效)
23
23
  * → 进程内存(无凭据服务的 Host)→ 环境。
24
24
  *
25
- * 令牌有效性检查(零额度探针:缺 `messages` 的请求预期 401 先于 400)属于
26
- * probe 路由的职责,本模块只管存取与状态;SPIKE.md §结论 5 记录了该探针
27
- * 尚待真机验证。
25
+ * **但「环境在最末」只在 service.resolve() 缺席或返回空时才成立**(实测 2026-10-05,
26
+ * dsh-credentials-local/lib/index.js:429):官方凭据服务的 precedence 里,**启动时的
27
+ * 环境快照压过存储文件**(`inherited(ref)` 用 `launchEnvironmentOf(ctx).getFrom(ref,
28
+ * ["process"])`,是启动时**冻结**的快照,不是活的 `process.env`)。所以当环境里有
29
+ * `MODELSCOPE_API_KEY` 时,「凭据服务」这一层返回的就是环境那个值,面板保存的值
30
+ * **赢不过它**——而面板仍会显示「已保存」。两个后果:
31
+ * ①在 DSH 启动**之后**改环境变量(含 Windows 用户级变量)不会被看见,改完必须
32
+ * **完全重启** DSH 才进快照;
33
+ * ②重启让环境变量生效后,它会**反过来压过**面板保存的值。
34
+ * 这不是本模块能修的(分层在 peer 的凭据服务里),但它必须写在手边。
35
+ *
36
+ * 令牌有效性检查(零额度探针:故意缺 `messages`)属于 probe 路由的职责,
37
+ * 本模块只管存取与状态。**真机已回填(2026-10-04,SPIKE.md §结论 5)**:魔搭
38
+ * 不先校验消息体,缺 `messages` 的请求返回 **200(空壳)而非 400**,鉴权失败才
39
+ * 是 401。所以探针判定规则定为 **401/403 = 令牌坏;200/400 = 令牌好;429 = 限频中**
40
+ * ——早先「401 先于 400」的预期已被推翻(见 probe.ts 文件头与本模块 §state 注释)。
28
41
  *
29
42
  * @module dsh-connect-modelscope-token-plan/ms-auth
30
43
  */
@@ -132,7 +145,8 @@ function createTokenStore({ credentials = null, env = process.env } = {}) {
132
145
  /**
133
146
  * 无秘密的状态描述(快照与面板消费)。
134
147
  * `valid` 恒为 null:本模块不做有效性断言(零额度探针在 probe 路由,
135
- * 尚待真机验证后接入——SPIKE.md §结论 5)。
148
+ * 语义已于 2026-10-04 真机回填——判定 401/403 = 令牌坏、200/400 = 令牌好;
149
+ * SPIKE.md §结论 5)。
136
150
  *
137
151
  * `ephemeral` 答的是「**手里这个值**重启会不会丢」,不是「有没有凭据服务」
138
152
  * ——wire 的定义是前者。曾经只按「服务缺席」置位,于是两种都说谎:没有服务
@@ -643,10 +657,13 @@ function parseRetryAfterMs(headers, message) {
643
657
  /**
644
658
  * 本地用量存储——面板「额度」tab 的数据真源。
645
659
  *
646
- * 为什么存在:魔搭没有余额查询接口(SPIKE.md §结论 1),面板的「剩余」=
647
- * 配置常数 − 本地计数。计数只可能来自**经本插件**的调用(probe 现在记、
648
- * provider 注册以后也记同一入口),所以面板文案必须永远带着「本地推算,
649
- * 仅统计经本插件的调用」。
660
+ * 为什么存在:官方只给**总**魔粒余额(`/openapi/v1/magicubes/balance`),不给
661
+ * 「哪个模型吃了多少、刚才谁在限频」。本 store 是那半边问题的答案——面板头条是
662
+ * 官方余额(真实值),本文件供的是分布 / 趋势 / 429 事件流。**不提供**「剩余次数」
663
+ * 推算:次数口径的上限是社区快照,与魔粒并排是误导,那条推算已从面板删除。
664
+ *
665
+ * 写入侧**只有** `usage-observer.ts`(在 provider 流出口观测真实对话)。面板上
666
+ * 没有任何入口能直接记一笔用量——早先 probe 路由的 `kind:"usage"` 试调已删除。
650
667
  *
651
668
  * 持久化纪律(与姊妹插件同款):`$DSH_HOME/state/[<profile>/]<name>/usage.json`
652
669
  * —— 按 profile 分段(它回答的是「这个 profile 的调用吃了多少额度」,与
@@ -1297,16 +1314,12 @@ function createInferenceClient({ settings, tokenStore, deps = {}, logger }) {
1297
1314
  }, settings.cacheSeconds * 1e3);
1298
1315
  },
1299
1316
  /**
1300
- * 一次 probe 调用。
1301
- *
1302
- * - usage:真调用(1 个 max_tokens 的 ping),**消耗 1 次免费额度**,
1303
- * 换回 usage 计数——面板「测试一次调用」与计数演示的来源;
1304
- * - validity:故意缺 messages 的请求,预期 401(令牌坏)先于 400
1305
- * (请求坏)返回,**零额度**鉴权探针(语义待真机回填,SPIKE.md)。
1317
+ * 一次 probe 调用:故意缺 messages 的请求,预期 401(令牌坏)先于 400
1318
+ * (请求坏)返回,**零额度**鉴权探针(语义已真机回填,SPIKE.md)。
1306
1319
  *
1307
1320
  * 失败抛 pluginError;调用方(probe 路由)负责把它记进事件流。
1308
1321
  */
1309
- async probe({ modelId, kind }) {
1322
+ async probe({ modelId }) {
1310
1323
  if (typeof modelId !== "string" || modelId.trim() === "") throw pluginError(CODE.CONFIG_ERROR, "probe: modelId is required");
1311
1324
  const { value: token } = await tokenStore.resolve();
1312
1325
  if (token === "") throw pluginError(CODE.AUTH_ERROR, "no ModelScope token configured — set MODELSCOPE_API_KEY or save one in the panel");
@@ -1315,13 +1328,6 @@ function createInferenceClient({ settings, tokenStore, deps = {}, logger }) {
1315
1328
  model: modelId,
1316
1329
  stream: false
1317
1330
  };
1318
- if (kind === "usage") {
1319
- payload.messages = [{
1320
- role: "user",
1321
- content: "ping"
1322
- }];
1323
- payload.max_tokens = 1;
1324
- }
1325
1331
  const response = await fetchWithTimeout(settings.apiBase + "/chat/completions", {
1326
1332
  method: "POST",
1327
1333
  headers: {
@@ -1331,49 +1337,23 @@ function createInferenceClient({ settings, tokenStore, deps = {}, logger }) {
1331
1337
  body: JSON.stringify(payload)
1332
1338
  });
1333
1339
  const elapsedMs = Date.now() - startedAt;
1334
- if (kind === "validity") {
1335
- if (response.status === 401 || response.status === 403) {
1336
- const message = await readErrorMessage(response);
1337
- throw pluginError(CODE.AUTH_ERROR, redactSecrets(message === "" ? "token rejected (HTTP 401/403)" : message));
1338
- }
1339
- if (response.status === 429) {
1340
- const message = await readErrorMessage(response);
1341
- const retryAfterMs = parseRetryAfterMs(response.headers, message);
1342
- throw pluginError(CODE.RATE_LIMITED, redactSecrets(message === "" ? "modelscope returned 429 (validity probe)" : message), retryAfterMs === null ? {} : { retryAfterMs });
1343
- }
1344
- if (response.status >= 500) throw pluginError(CODE.UPSTREAM_ERROR, "modelscope returned HTTP " + String(response.status) + " (validity probe)");
1345
- return {
1346
- ok: true,
1347
- kind,
1348
- status: response.status,
1349
- modelId,
1350
- usage: null,
1351
- elapsedMs
1352
- };
1340
+ if (response.status === 401 || response.status === 403) {
1341
+ const message = await readErrorMessage(response);
1342
+ throw pluginError(CODE.AUTH_ERROR, redactSecrets(message === "" ? "token rejected (HTTP 401/403)" : message));
1353
1343
  }
1354
- if (!response.ok) {
1344
+ if (response.status === 429) {
1355
1345
  const message = await readErrorMessage(response);
1356
- if (response.status === 429) {
1357
- const retryAfterMs = parseRetryAfterMs(response.headers, message);
1358
- const triage = classifyRateLimit(message);
1359
- const extra = retryAfterMs !== null && retryAfterMs !== void 0 ? { retryAfterMs } : {};
1360
- throw pluginError(triage === "quota" ? CODE.QUOTA_EXCEEDED : CODE.RATE_LIMITED, redactSecrets(message === "" ? "modelscope returned 429 (probe on " + modelId + ")" : message), extra);
1361
- }
1362
- throw pluginError(classifyStatus(response.status), redactSecrets(message === "" ? "modelscope returned HTTP " + String(response.status) : message));
1346
+ const retryAfterMs = parseRetryAfterMs(response.headers, message);
1347
+ const triage = classifyRateLimit(message);
1348
+ const extra = retryAfterMs !== null && retryAfterMs !== void 0 ? { retryAfterMs } : {};
1349
+ throw pluginError(triage === "quota" ? CODE.QUOTA_EXCEEDED : CODE.RATE_LIMITED, redactSecrets(message === "" ? "modelscope returned 429 (probe on " + modelId + ")" : message), extra);
1363
1350
  }
1364
- const json = await response.json().catch(() => null);
1365
- const usage = json === null ? null : json.usage;
1366
- const positive = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
1351
+ if (response.status >= 500) throw pluginError(CODE.UPSTREAM_ERROR, "modelscope returned HTTP " + String(response.status) + " (validity probe)");
1352
+ response.body?.cancel();
1367
1353
  return {
1368
1354
  ok: true,
1369
- kind,
1370
1355
  status: response.status,
1371
1356
  modelId,
1372
- usage: usage === void 0 || usage === null ? null : {
1373
- promptTokens: positive(usage.prompt_tokens) ?? 0,
1374
- completionTokens: positive(usage.completion_tokens) ?? 0,
1375
- totalTokens: positive(usage.total_tokens) ?? 0
1376
- },
1377
1357
  elapsedMs
1378
1358
  };
1379
1359
  }
@@ -2106,7 +2086,7 @@ function createProviderPublisher(deps = {}) {
2106
2086
  const { settings, panelSwitch, loadAdapterModule, getLlm, resolveApiKey, usage, emit, logger } = deps;
2107
2087
  const effectiveSettings = settings ?? {};
2108
2088
  const effectivePanelSwitch = panelSwitch ?? (async () => null);
2109
- const effectiveLoadAdapterModule = loadAdapterModule ?? (() => import("./llm-adapter-CpcQ3gC5.js"));
2089
+ const effectiveLoadAdapterModule = loadAdapterModule ?? (() => import("./llm-adapter-DnaNNORe.js"));
2110
2090
  const effectiveGetLlm = getLlm ?? (() => null);
2111
2091
  const effectiveResolveApiKey = resolveApiKey ?? (async () => "");
2112
2092
  const effectiveLogger = logger ?? { warn: () => {} };
@@ -2785,14 +2765,20 @@ function registerTokenRoute(ctx, wiring) {
2785
2765
  //#endregion
2786
2766
  //#region src/host/routes/probe.ts
2787
2767
  /**
2788
- * probe 路由:面板「接入」tab 的「测试一次调用」,也是本地计数器的第一个
2789
- * 数据源。
2768
+ * probe 路由:面板「接入」tab 的「验令牌(零额度)」按钮。
2790
2769
  *
2791
- * 两种形态(SPIKE.md §结论 5):
2792
- * - `kind:"usage"` —— 真调用(max_tokens=1),**消耗 1 次免费额度**;成功后
2793
- * recordCall 把这次调用记进当天天桶与单模型计数(tokens 取 usage)。
2794
- * - `kind:"validity"` —— 故意缺 messages 的请求,预期 401 先于 400,零额度
2795
- * 鉴权探针(语义待真机回填)。只记事件,不记调用。
2770
+ * 只剩一种形态(SPIKE.md §结论 5,2026-10-04 真机回填):
2771
+ * - 故意缺 messages 的请求——魔搭**不先校验消息体**,实测返回 200(空壳)而非
2772
+ * 400,所以判定规则是 **401/403 = 令牌坏;200/400 = 令牌好;429 = 限频中**。
2773
+ * 早先「401 先于 400」的预期已被真机推翻(空壳响应 `created:0`,疑似零额度,
2774
+ * 未证实——故 validity 结果**不计入**本地用量,也不记调用,只记事件)。
2775
+ *
2776
+ * `kind:"usage"`(真调用、max_tokens=1、消耗 1 次免费额度)**已删除**:它只服务
2777
+ * 已随面板收敛删掉的「模型目录」逐行试调按钮,且**不在 DSH 的调用路径上**——真
2778
+ * 调用经 provider adapter,由 `usage-observer.ts` 在流出口记账。删它的另一个理由
2779
+ * 是形态本身:服务端原有 `kind = body.kind === "validity" ? "validity" : "usage"`
2780
+ * 的**缺省即 usage**,任何人 POST 一个 modelId 就会触发一次真实计费调用。面板只
2781
+ * 剩零额度探针之后,缺省不该指向「花钱的那个」。
2796
2782
  *
2797
2783
  * 失败照常记账:429 分诊成 quota/rate_limit 事件(带 Retry-After),401 记
2798
2784
  * auth 事件——事件流是面板「额度」tab 的第三块。错误消息先脱敏(红线 1)。
@@ -2812,7 +2798,6 @@ function registerProbeRoute(ctx, wiring) {
2812
2798
  const body = await readJsonBodyOr400(request, response);
2813
2799
  if (body === null) return;
2814
2800
  const modelId = typeof body.modelId === "string" ? body.modelId.trim() : "";
2815
- const kind = body.kind === "validity" ? "validity" : "usage";
2816
2801
  if (modelId === "") {
2817
2802
  writeJson(response, 400, {
2818
2803
  ok: false,
@@ -2821,14 +2806,7 @@ function registerProbeRoute(ctx, wiring) {
2821
2806
  return;
2822
2807
  }
2823
2808
  try {
2824
- const result = await inference.probe({
2825
- modelId,
2826
- kind
2827
- });
2828
- if (kind === "usage") await usageStore.recordCall({
2829
- modelId,
2830
- tokens: result.usage === null ? null : result.usage.totalTokens
2831
- });
2809
+ const result = await inference.probe({ modelId });
2832
2810
  writeJson(response, 200, {
2833
2811
  ...result,
2834
2812
  tokenState: await wiring.tokenStore.state()
@@ -1,4 +1,4 @@
1
- import { C as redactSecrets, _ as reclassifyStream, c as LLM_PROVIDER_ID, i as name, l as buildDescriptors, m as CODE, o as LLM_API_KEY_NAME, s as LLM_DISPLAY_NAME } from "./host-config-DBCiZOBW.js";
1
+ import { C as redactSecrets, _ as reclassifyStream, c as LLM_PROVIDER_ID, i as name, l as buildDescriptors, m as CODE, o as LLM_API_KEY_NAME, s as LLM_DISPLAY_NAME } from "./host-config-C4bWNPWU.js";
2
2
  import { createProvider } from "@earendil-works/pi-ai";
3
3
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
4
4
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
@@ -274,10 +274,11 @@ function assemblePiAiAdapter({ providerId, displayName, apiKeyName, models, reso
274
274
  * usage-store 的天桶/单模型计数与 429 事件流。
275
275
  *
276
276
  * 为什么不是「在 adapter 里随便挂个钩子」,而是独立一层:M4 之前
277
- * `usageStore.recordCall` 的唯一调用者是 `routes/probe.ts`,而面板的 usage
278
- * 试调按钮已随目录表删除、validity 探针按设计不记调用——于是本地计数层在
279
- * 接入 provider 之后**没有任何生产者**,「今日次数 / 单模型分布 / 趋势 /
280
- * 429 事件流」四块面板恒为 0 与空。本模块补上这条缺失的生产者。
277
+ * `usageStore.recordCall` 的唯一生产者是 `routes/probe.ts` 的 usage 试调,而那个
278
+ * 按钮已随目录表删除、validity 探针按设计不记调用——于是本地计数层在接入
279
+ * provider 之后**没有任何生产者**,「今日次数 / 单模型分布 / 趋势 /
280
+ * 429 事件流」四块面板恒为 0 与空。本模块补上这条缺失的生产者,也就成了**唯一**
281
+ * 的常驻写入侧(`probe` 路由的 `kind:"usage"` 分支随后一并删除,见 probe.ts)。
281
282
  *
282
283
  * ## 与 harness 自己的账本的分工(别再造第二本账)
283
284
  *
@@ -315,8 +316,13 @@ function assemblePiAiAdapter({ providerId, displayName, apiKeyName, models, reso
315
316
  * 一次被打上限频的请求同样是一次上游调用。token 数在上游给出时带上,给不出
316
317
  * 时传 `null`(wire 语义基线:null 是「上游没给」,绝不写成 0)。
317
318
  *
318
- * 已知偏差(方向是少记,不是多记):peer 的重试在同一条流**内部**重发 HTTP,
319
- * 观察器只看到一条流,于是重试掉的那些次不单独计。宁可少记也不虚增。
319
+ * 已知偏差(不是「少记」,是「按流计次」):重试不在一条流内部发生,而在
320
+ * `dsh-agent-loop` 消费完流之后由 `agent/request-error` 瀑布决定
321
+ * (`dsh-agent-loop/lib/index.js:1116-1134`),`dsh-llm-retry` 据此**重新发起一次
322
+ * 请求**、产生**新的流**。观察器套在流出口上,于是**每次重试各记一次 call**——
323
+ * 按「魔搭按次数计费」的口径这反而是对的(每次重发都真打了一次上游),但它与
324
+ * 早先本文件「重试掉的那些次不单独计」的说法相反,故此处一并更正。
325
+ * 2026-10-05 读 peer 源码(`@deepseek-ai/dsh-llm-retry`、`dsh-agent-loop`)核实。
320
326
  *
321
327
  * peer-free:只 import 本仓库的 peer-free 模块(`llm-error-fix` / `util`)与
322
328
  * `shared/wire` 的类型,离线可测——与 `llm-error-fix` 同一纪律。
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-modelscope-token-plan",
3
3
  "displayName": "DSH Connect ModelScope Token Plan",
4
4
  "icon": "./icon.svg",
5
- "version": "0.1.0",
5
+ "version": "0.2.0",
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"
@@ -41,7 +41,7 @@
41
41
  "typecheck": "tsc -p tsconfig.json",
42
42
  "build:client": "tsdown -c tsdown.config.mjs",
43
43
  "duplicate-check": "jscpd src --min-tokens 70 --reporters console-full",
44
- "test": "node test/wire.test.mjs && node test/config.test.mjs && node test/state-store.test.mjs && node test/usage-store.test.mjs && node test/routes.test.mjs && node test/provider.test.mjs && node test/inference-vision.test.mjs && node test/provider-routes.test.mjs && node test/provider-publish.test.mjs && node test/panel.test.mjs && node test/doctor.test.mjs && node test/catalog.test.mjs && node test/switch-precedence.test.mjs && node test/credentials.test.mjs && node test/codes.test.mjs && node test/retry.test.mjs && node test/error-fix.test.mjs && node test/usage-observer.test.mjs && node test/release.test.mjs && node test/build-gate.mjs",
44
+ "test": "node test/wire.test.mjs && node test/config.test.mjs && node test/docs.test.mjs && node test/state-store.test.mjs && node test/usage-store.test.mjs && node test/routes.test.mjs && node test/provider.test.mjs && node test/inference-vision.test.mjs && node test/provider-routes.test.mjs && node test/provider-publish.test.mjs && node test/panel.test.mjs && node test/doctor.test.mjs && node test/catalog.test.mjs && node test/switch-precedence.test.mjs && node test/credentials.test.mjs && node test/codes.test.mjs && node test/retry.test.mjs && node test/error-fix.test.mjs && node test/usage-observer.test.mjs && node test/release.test.mjs && node test/build-gate.mjs",
45
45
  "doctor": "node tools/doctor.mjs",
46
46
  "doctor:json": "node tools/doctor.mjs --json",
47
47
  "build": "tsdown -c tsdown.config.mjs",