dsh-connect-modelscope-token-plan 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +116 -74
- package/README.md +23 -12
- package/assets/tab-account-and-usage.png +0 -0
- package/assets/tab-model-draw.png +0 -0
- package/assets/tab-model-list.png +0 -0
- package/client.js +778 -182
- package/cordis.patch.yml +13 -4
- package/lib/host-config.js +271 -47
- package/lib/index.js +1575 -567
- package/lib/llm-adapter.js +38 -18
- package/package.json +2 -2
- package/screenshots.json +2 -1
- package/assets/icon-preview.png +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,105 +1,147 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> 本文件只记公开行为变化(新增能力、破坏性改动、重要修复)。实现细节、重构与测试加固请直接看 git log。
|
|
4
|
+
>
|
|
5
|
+
> **判断标准**:写下一条之前问一句——**用户能观察到它吗?** 能(面板变了字、流程多了步、
|
|
6
|
+
> 数据不再丢)才进本文件;不能(改了哪个文件、加了几条测试、抽了哪个函数)进 commit message
|
|
7
|
+
> 或 `docs/archive/`。**本文件是给用户看的,不是给维护者看的。**
|
|
8
|
+
>
|
|
9
|
+
> **写法**
|
|
10
|
+
>
|
|
11
|
+
> - **主题化要点**,不是 `Features/Fixes` 分组。一个主题一个粗体小节,下面 1–4 条要点。
|
|
12
|
+
> **一条 ≤ 120 字,讲清「变了什么 + 用户看到什么」**;病灶取证、判据推导、阴性对照一律不写。
|
|
13
|
+
> - **大小是信号**:一节 20–30 行是正常体量,超过就说明**混进了实施过程**——把它搬走。
|
|
14
|
+
> 既有的长节(如 `[0.2.0]`)是历史债,**别再照着写**。
|
|
15
|
+
> - **摘要必须自含主语与后果**:`snapshot-aggregate.ts` 把空清单当不过滤 ≠
|
|
16
|
+
> 「面板说 allowed:"all" 却按旧清单注册」。写**后者**——前者是病因,读者要的是症状。
|
|
17
|
+
> - **不许沉默**:修好一个从未生效的分支、或改掉一个会误导人的读数,也算行为变化,
|
|
18
|
+
> 照实上报(例:`[0.3.0]` 的「读不到」不再被画成 0)。**不许拿「内部修复」当不写进
|
|
19
|
+
> 本文件的理由**。
|
|
20
|
+
> - **测试/门禁事项不单独成节**:`npm test` 多几个套件、CI 换了工具链版本,用户看不见。
|
|
21
|
+
> 确有影响就并在「工程」一行里,细节留在 commit 或归档。
|
|
22
|
+
>
|
|
23
|
+
> **过程材料去哪**:`docs/archive/YYYY-MM-DD-X.Y.Z-process.md`。发版时新开一节,条目里
|
|
24
|
+
> 带一句外链(见各节末)。**不要为了少写一节而把过程删掉**——那是丢证据,不是精简。
|
|
25
|
+
>
|
|
26
|
+
> **发版时**:顶部开 `## [X.Y.Z] — YYYY-MM-DD`,完整流程见
|
|
27
|
+
> [RELEASING.md](RELEASING.md) §3(本文开头这段是**写作者规范**,那是**发布操作流程**)。
|
|
28
|
+
|
|
29
|
+
## [0.5.0] — 2026-10-17
|
|
30
|
+
|
|
31
|
+
**面板「模型」tab:清单按厂商分组**
|
|
32
|
+
|
|
33
|
+
- 三十几个模型平铺时,`deepseek-ai/…` 这类前缀每一行都要说一遍(目录只回 id,行名就等于 id)。现在按厂商分组:组头一行(可折叠,带数量与已选数),组内只显示模型名、完整 id 留在悬停里;顶部加了搜索框;推荐的三家(深度求索 / 智谱 / 通义千问)置顶。
|
|
34
|
+
- 「推荐」以前挂在每一行上,同名厂商的十几行各一枚;现在每组建一枚、挂在组头——它同时解释了这一组凭什么是置顶的。
|
|
35
|
+
- 卡片顶部原先把「已接入」「已注册」分两行说,加上区块标题一共说了三遍;现在并成一行「已接入 · 已注册(模型可在选择器里选用) · 面板」。失败仍单独一行,且此时不再重复一句状态。
|
|
36
|
+
- 出图卡:能力改正向双徽章(文生图 / 图生图各一枚),层级重排为「状态 → 选择 → 操作 → 说明」,说明沉底;「自动选择」成为单选组里的第一项。
|
|
37
|
+
|
|
38
|
+
**面板「额度」tab:趋势图与读数**
|
|
39
|
+
|
|
40
|
+
- 趋势图改为**竖向柱状图**,柱顶标当天次数(0 次不标),轴约 7 个日期刻度且不带年份。
|
|
41
|
+
- 新增**「显示天数」**选择器(默认 / 7 / 14 / 30 / 90),并与决定「能回看多久」的**保留窗口**(90 天)拆成两件事——显示窗口恒被夹在保留窗口内,不会出现「把已剪掉的日子画成 0」这种与「那天真的没调用」无法区分的形状。选择保存在 `usage-prefs.json`。
|
|
42
|
+
- 单模型排行的读数补上单位:`2 · 21,695t` → `2 次 · 21,695 token`(裸 `t` 既不是量级后缀,也不是任何语言的单位词)。
|
|
43
|
+
|
|
44
|
+
**读数与标签不再自相矛盾**
|
|
45
|
+
|
|
46
|
+
- 窗口徽标此前一律按 ÷1024 换算,把上游自己写的十进制数印成了另一个样子:`256000`(上游叫 256K)显示 250K、`1000000`(GLM-5.2 的 1M)显示 977K、`200000` 显示 195K。现在按**上游写出来的进制**渲染:2 的幂仍按 1024(`131072` → 128K),十进制仍按 1000(`128000` → 128K)。代价是两个进制都整除的 `128000` 与 `131072` 同读 128K(相差 2.4%)。
|
|
47
|
+
- **本地计数是「本客户端」的口径**:桌面端与 Web 端是两个 profile,各记各的账、互不可见。此前面板图例与 README 把它写成「覆盖全部 DSH 魔搭调用」,实测不成立。想并排看各 profile 的账用 `npm run doctor`。
|
|
48
|
+
- 成本更诚实的两处:策展表(内置窗口真值)距上次核对超过 180 天时,面板会给一条「该复核了」的提示,静态表不再悄悄腐烂;魔搭目录读不到时不再被当成「0 个模型」,保存也不再因此报出假失败。
|
|
4
49
|
|
|
5
|
-
|
|
50
|
+
**对上游更省、更不打断**
|
|
6
51
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
- **轮询同标**(复核抓出的 HIGH,发版前修掉):`runPoll` 起初无参调 `fetchModels`,用户保存覆盖后每 `pollSeconds` 轮询都会用未打标条目 publish——`catalogSignature` 含窗口值,签名不同触发摘下重挂,覆盖静默回退、适配器每轮重建。修复后 fetch 前读落盘覆盖传入;fetch 期间覆盖被改则跳过本轮(保存路由已 publish 正确值,防倒退)。回归钉子:`test/deps-wiring.test.mjs` §3(注入 `loadAdapterModule` 假模块驱动真 publish 链,断言工厂收到的条目带覆盖;阴性对照:修复前源码下该断言红)。
|
|
10
|
-
- **roster 行带窗口标注**:每行标当前生效窗口(`formatWindow`,128000 诚实显示 125K 不美化);策展表来的值带 `*` 号与悬停说明(平台未亲口声明),与目录声明/用户覆盖区分。快照 `provider` 块新增 `contextWindow`(正常块/降级形状/`wire.ts`/client `providerOf` 四处同步)。
|
|
11
|
-
- **类型层**:`ProviderRouteWiring.inference.fetchModels` 签名扩为 `(contextWindow?: number | null)`;publisher 的 `loadAdapterModule` 离线测试缝接进 `HostDeps`(真模块 import 的 peer 只在 Host 进程存在,测试进程必败)。`test/context-catalog.test.mjs` 钉策展表形状与命中语义。
|
|
52
|
+
- 模型详情标签此前与目录、余额共用 60 秒缓存,于是每轮轮询都对目录里**每个**模型打一次网站接口(约 38 个请求/分钟、5 万/天,只为判一个几乎不变的布尔值)。标签改走自己的 6 小时缓存。
|
|
53
|
+
- 面板里改模型展示名不再触发重新注册(此前那会打断在途的对话请求);出图等待在关闭/卸载时立即中断,不再让用户等满那个 15 秒间隔;「忘掉已保存的令牌」不再可能因删除失败而静默复活环境变量里的旧令牌。
|
|
12
54
|
|
|
13
|
-
|
|
55
|
+
> **实施过程(病灶、取证、判据)在
|
|
56
|
+
> [docs/archive/2026-10-17-0.5.0-process.md](docs/archive/2026-10-17-0.5.0-process.md)**——这一轮
|
|
57
|
+
> 里有多处是「推翻自己此前的明文裁定」,理由与代价都写在里面。
|
|
14
58
|
|
|
15
|
-
|
|
16
|
-
- **刻意没有百分比,也没有黄色档**:魔粒余额是存量、官方没有发布任何上限或低额阈值,画一条进度条或定一个「低于 N 魔粒算黄」的阈值,都是发明一个没人公布过的分母。三档只剩:绿(读数是正数)、红(官方读数确实是 0)、灰(读不到)。
|
|
17
|
-
- **`null` ≠ `0` 一路贯彻到胶囊**:读不到渲染「—」,绝不渲染 0;0 是读数(余额确实耗尽),走告警色。读失败时保留上一次的数值并加「上次数据」标记(陈旧阈值 = Host 声明的 `pollSeconds` × 3,默认 30s → 90s;「从没读到」不算陈旧)。
|
|
18
|
-
- **门控在组件边界**:外层只调一次 `useProjection("modelSelection")` 决定内层挂不挂,轮询 hook 全在内层,切模型不会踩 hook 顺序(内层被摘即停轮询,非归属会话一个请求都不发)。归属判定用**非空字符串**判 `next`,空 `next`(`{}` / `{provider:""}`)不许遮住仍可用的 `lastUsed`——这条规则是本次新写的套件当场抓出来的缺陷:第一版照「`typeof provider === "string"` 即算可用」写,`{provider:""}` 是字符串,于是切模型那一帧胶囊消失。
|
|
19
|
-
- 外观全在 `styles.ts` 的 `S.pill*` 一族(本插件不注入样式表),按宿主工具栏尺度定:28px 高 / 12px 字 / 无框 / 透明底 / 只取宿主 token。其中的坑已写进注释:无框必须 `borderStyle:"none"` + `borderWidth:0` **分开写**,写成 `border:"none"` 时 UA 的 border-image 仍会画出一圈框。
|
|
20
|
-
- **门禁**:新增 `test/composer-pill.test.mjs`(纯规则面:座席只注册一次、provider 字面量与 `src/host/llm-models.ts` 的 `LLM_PROVIDER_ID` 逐字一致、门控四态、`null`≠0、三档定级、陈旧边界、字典与样式族),并给 `test/dom-render-react.test.mjs` 加了 §6(真 reconciler 下:非归属整块缺席且零请求、空 `next` 不遮 `lastUsed`、两个读数上屏、28px 与无框真的进了 CSSOM、三档圆点墨色互不相同、失败保留上次读数并标「上次数据」、首帧「读取中」不是 0)与 §7(胶囊卸载只清掉自己的 interval)。套件数 31(30 个 `*.test.mjs` + `build-gate`)。
|
|
21
|
-
- **顺带修**:胶囊模块头部原先把口径纪律指向一个不存在的 `docs/usage-tracking.md`;本仓库没有独立的计量文档,唯一家是 README「诚实声明」一节,指针已改。
|
|
59
|
+
## [0.4.0] — 2026-10-17
|
|
22
60
|
|
|
23
|
-
|
|
61
|
+
**面板保存不再被瞬时网络故障打断**
|
|
24
62
|
|
|
25
|
-
-
|
|
26
|
-
-
|
|
63
|
+
- 在面板里切换 provider 开关、保存模型清单或上下文窗口时,若恰逢魔搭目录拉取失败,此前的实现会把失败降级成「空目录」并照样发布——已注册的模型会从选择器里全部消失,直到下一轮轮询才恢复。现在这种故障只保留当前已服务的模型并记录原因,下一次收敛自然重试。
|
|
64
|
+
- 保存动作与后台轮询此前的两套互不知情的发布逻辑,现已合并为单一的收敛流程:保存后立即生效,不再出现「保存了却要等下一轮轮询才兑现」、或生效值被旧轮询覆盖回去一个周期的现象。
|
|
27
65
|
|
|
28
|
-
|
|
66
|
+
**出图工具不再让整轮对话干等**
|
|
29
67
|
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
- 这一档没覆盖的部分见下一条(真 React 那一档),以及 `docs/ROADMAP.md` 的 Backlog:并发与错误边界、fake timers 下跑满 cadence 的长周期行为。
|
|
68
|
+
- 出图工具此前会一直 `await` 到出图完成才返回(最长 15 分钟),期间这轮对话什么都做不了。现在一次调用只等约 90 秒(新增 `drawWaitMs` 可调):图好了直接返回图片,没好就返回「仍在生成中」加一个 `jobId`,稍后带上它再调用即可继续查询——只查询,不会重复出图。
|
|
69
|
+
- 15 分钟总预算仍然是「这张图值不值得继续等」的上限,但改为**从提交时刻起累计**(跨多次续查),超过才放弃;放弃时依旧会说明上游任务可能仍在生成,而不是报「出图失败」。
|
|
33
70
|
|
|
34
|
-
|
|
71
|
+
**出图设置的保存不再丢,失败消息里的密钥不再漏**
|
|
72
|
+
|
|
73
|
+
- 出图开关与出图模型两个设置此前各存各的、互不排队:快速改两项时,后写的那次会盖掉前一次——界面报成功,其中一项却没落盘。现在两次写入串行,两个都能存住。
|
|
74
|
+
- 出图设置文件**存在却读不懂**时(更新的构建写的、或文件损坏),此前会被当成「没设过」并照写照存,把盘上的设置覆盖掉。现在与 provider/用量两处同纪律:读不懂就拒写并说明原因;读侧把「保存的值被忽略」明说成一条警告,面板不再把它呈现成「从未保存过」;`npm run doctor` 也终于会盘点这个文件。
|
|
75
|
+
- 出图失败消息的脱敏此前用的是本模块私有的一份正则副本,比插件其余部分少两组规则(`sk-` 密钥、`password=…` 形式的键值对)——上游把请求体原样回显时,凭据可能一路进面板与日志。现在全插件共用同一份。
|
|
76
|
+
|
|
77
|
+
**provider 状态读不到时,面板不再拿编出来的值顶上**
|
|
78
|
+
|
|
79
|
+
- 此前「接入为 DSH 模型」那一块读不到时,插件会塞一份填满默认值的块(开关关、0 个模型、允许清单「不过滤」)。那个形状看起来就是一个答案,面板因此不再去单独查询 provider 状态,只能按它渲染——而真相是「读不到」。现在这一块会被整个省略,面板随即改用独立的状态查询取真值,两次都取不到才显示「未接入」。
|
|
80
|
+
- 省掉的块不再把原因一起带走:这类失败会作为一条警告列出,操作者能区分「没接入」与「插件读不到自己的状态」。
|
|
81
|
+
|
|
82
|
+
**「全部隐藏」的口径统一**
|
|
83
|
+
|
|
84
|
+
- 允许清单里同时含「全部隐藏」哨兵和真实模型 id 时(手工编辑 `provider.json` 会出现),现在与面板显示一致地按「什么都不提供」处理;此前个别路径会把其中的真实 id 放出来,面板计数与实际提供的模型不符。
|
|
85
|
+
|
|
86
|
+
> **实施过程(病灶、取证、判据)在
|
|
87
|
+
> [docs/archive/2026-10-17-0.4.0-process.md](docs/archive/2026-10-17-0.4.0-process.md)**——那几轮里
|
|
88
|
+
> 不少是踩过的坑,照着做能绕开。
|
|
89
|
+
|
|
90
|
+
## [0.3.0] — 2026-10-16
|
|
35
91
|
|
|
36
|
-
|
|
37
|
-
- 阴性对照:同一批 3 条变异(把 `tabActive` 压平、给令牌区块塞一个不稳定 key、删掉 roster 行的 `key`),真 React 档抓 **3** 条,mini 档抓 **0** 条;其中「删 key」是被**零告警**那条抓住的,不是被顺序断言抓住(checkbox 全受控,删 key 不串位)——凡「React 自己的规矩」类缺陷都归零告警那条。spec:`tools/dev/mutate-dom-render-react.json`。
|
|
38
|
-
- **代价是版本耦合**:pin 的是宿主浏览器实际注入的那一份 React,用 `tools/dev/probe-host-react.mjs` 从 `app.asar` 读出来(2026-10-15 实测 18.3.1),套件启动时把 `React.version` 与期望值硬比,漂了就红并让人先去核对——不这么办的话,门禁会静默测着一个旧的 reconciler。产物里仍然永远没有 React(`tsdown` 的 `neverBundle` 不变),被钉的只是测试副本;这与 `types/dsh-peer-modules.d.ts`「不声明 React 形状」的立场不冲突。
|
|
39
|
-
- 两个必踩的坑写进了套件文件头(别删注释):`react-dom/client` 必须在 `window`/`document` 就位之后再 import,否则它按「没有 DOM」求值,input 事件被整条吞掉、payload 变空串,看着像被测代码坏了;受控 input 要用原型上的原生 setter 改值,否则被 React 的值追踪器判成「没有变化」。写第一版时当场踩了前者。
|
|
92
|
+
**新增「上下文窗口」全局档位**
|
|
40
93
|
|
|
41
|
-
|
|
94
|
+
- 魔搭目录 `/v1/models` 不返回任何窗口字段,此前每个模型一律按 128K 兜底声明。新增面板「上下文窗口」选择器:默认 / 128K / 256K / 512K / 1M,落盘 `provider.json`。窗口取值按三级真值链——目录真声明过就用目录值,其次是用户档位,再次是内置策展表(models.dev 交叉核对,当前目录 19 个 id 有数据),都没有才落 128K。
|
|
95
|
+
- roster 每行标出当前生效窗口(128000 诚实显示 125K,不美化);策展表来的值带 `*` 号与悬停说明(平台未亲口声明),与目录声明、用户覆盖区分开。
|
|
96
|
+
- **修复**:保存窗口覆盖后,每 `pollSeconds` 轮询都会用未打标条目重新 publish,覆盖被静默回退、适配器每轮重建。现在轮询读落盘覆盖传入;fetch 期间覆盖被改则跳过本轮。
|
|
42
97
|
|
|
43
|
-
|
|
44
|
-
- **版本守卫 fail-open**:`readStateVersion` 把 EACCES/EBUSY/EIO(以及路径被目录占住)与「文件不存在」一起折成 `null`,而写守卫只把 `null` 读作「没有东西要保护」——ADR-006「绝不覆盖读不懂的文件」在自己那一侧漏了:**存在却读不出**的文件被判为可写。现在只有 ENOENT 返回 `null`(写是恢复手段),其余 fs 错误抛给写守卫;`provider-store` / `usage-store` 据此拒写并说明理由(usage 侧复用既有的只读闩锁,文件恢复可读即复位)。钉子:`state-store.test.mjs` §6、`provider.test.mjs` §10(阴性对照:`notStrictEqual` 红)。
|
|
45
|
-
- **回滚路径漏 release**:`restorePreviousPair` 的 catch 只清 `built`/`registered`,而 `registerProviderPair` 是**先 registerAdapter 再挂目录行**——后者抛错时 `state.releaseAdapter` 已经握着一次成功的注册,于是「面板说不注册、Host 里却还在路由」。现在那条 catch 先 `release()`。钉子:`provider-publish.test.mjs` §12。
|
|
46
|
-
- **回滚成功后 `error` 不清,空转守卫形同不存在**:注册失败回滚恢复旧对之后,若恢复出来的 offer 与本次请求**相同**,`error !== null` 会让两处空转守卫永远跳过不了——每个轮询周期把同一对摘下再挂上(fix B 明确要避免的「期间在途请求可能被打断」),面板还挂着一个已不成立的错误。现在只有「请求的 offer 确实没兑现」才留 `error`。钉子:`provider-publish.test.mjs` §11(阴性对照:`actual 'transient registration failure'` / `expected null`)。
|
|
47
|
-
- **成功的 probe 被报成失败**:成功分支里 `await tokenStore.state()` 住在 try 内,凭据服务读状态一抛错就落进 catch——一次已成功的验令牌回报 `ok:false`,还往面板事件流写一条假 `error`(事件流是该面板的独家数据源)。补 `.catch(() => null)`,与失败分支同形。钉子:`routes.test.mjs` 的 probe 节(阴性对照:`actual false` / `expected true`)。
|
|
48
|
-
- **「忘掉令牌」整条丢掉 body.ok**:`postJson` 只把 HTTP/解析失败折成 `null`,域失败是 `{ok:false, error}`;`saveToken` 与 provider 写入都检查了,唯独「忘掉令牌」直接往下走。而 Host 侧 `ms-auth.forget` 的失败是**刻意传播**的(凭据文件只读时抛错,其文件头写着「谎报成功比失败更糟——用户以为凭据已删干净」),于是界面上表现为「什么都没发生」。三个写回调收敛到 `http.ts#writeFailure` 一份判据。钉子:`panel.test.mjs` §8/§8b(阴性对照:§8b 的 `match` 红)。
|
|
98
|
+
**新增对话框用量胶囊**
|
|
49
99
|
|
|
50
|
-
|
|
100
|
+
- 会话当前选中的 provider 是 `modelscope-token-plan` 时,输入框工具栏出现胶囊,正文两个读数、各标各的来源:`魔搭 · 143 魔粒 · 今日本地 7 次`。前半是官方魔粒余额,后半是本地日桶——「本地」二字是承重的,官方接口没有「今日」这一项(口径见 README「诚实声明」)。
|
|
101
|
+
- **刻意没有百分比,也没有黄色档**:魔粒余额是存量,官方未发布任何上限或低额阈值;画进度条或定阈值都是发明一个没人公布过的分母。三档只剩绿(读数是正数)、红(官方读数确实是 0)、灰(读不到)。
|
|
102
|
+
- **读不到渲染「—」,绝不渲染 0**;读失败时保留上一次数值并标「上次数据」(陈旧阈值 = `pollSeconds` × 3,默认 90s)。
|
|
103
|
+
- 非归属会话一个请求都不发;切模型不会踩 hook 顺序。
|
|
51
104
|
|
|
52
|
-
|
|
53
|
-
- 令牌输入框缺 `aria-label`、ToggleSwitch 缺 `busyLabel`:屏幕阅读器读不出「正在切换」。补两条 i18n 键(zh/en 各 +1)。
|
|
54
|
-
- `modelscopeModelUrl` 用 `encodeURI` 不转义 `#`/`?`,模型 id 含这些字符时 URL 语义被破坏。改为分段 `encodeURIComponent`。
|
|
55
|
-
- `modelCount`/`enabledCount` 的 `typeof === "number"` 对 NaN 也返回 true,面板会显示「NaN 个模型」。补 `Number.isFinite`。
|
|
56
|
-
- 嵌套降层:`swapRegistration` 的 rollback 路径从 4 层 try/catch 降到 2 层(提取 `restorePreviousPair`);`publishProviderOnce` 两处空转守卫的重复签名比对提取为 `offerUnchanged()`;`runPoll` 的 fallback 逻辑提取为 `currentEnabledIds()`。
|
|
57
|
-
- 新增 `docs.test.mjs` §11 引用有效性门禁:扫描 `src/` 与 `test/` 里所有 `test/*.test.mjs` 引用,文件不存在即失败。补 HIDE_ALL_MODELS 双端交叉断言与 locale key 集合一致性断言。三处恒真/死断言换成真断言。
|
|
58
|
-
- 六处 `test/peer-contract.test.mjs` 虚假引用全删(文件从未存在,但注释声称 peer 契约被它钉死),改为诚实的缺口声明并记入 ROADMAP backlog。`find-fake-gates.mjs` 误报率从 200+ 降到 3 条。
|
|
105
|
+
**六处写守卫与注册状态机缺陷**(每处都有用户可见后果)
|
|
59
106
|
|
|
60
|
-
|
|
107
|
+
- **面板与 Host 对「正在提供什么」各说一套**:操作者按提示删掉 `provider.json`(或清单被保存为空)后,面板说 `allowed:"all"` 且数上全部模型,轮询却继续按内存里的旧清单注册。
|
|
108
|
+
- **版本守卫 fail-open**:**存在却读不出的** `provider.json` / `usage.json` 被判为可写,违反 ADR-006「绝不覆盖读不懂的文件」;现在只有文件不存在才返回 `null`,其余 fs 错误一律拒写并说明理由。
|
|
109
|
+
- **回滚路径漏 release**:注册回滚时适配器已注册成功,「面板说不注册、Host 里却还在路由」。
|
|
110
|
+
- **回滚成功后 `error` 不清**:每个轮询周期把同一对摘下再挂上(期间在途请求可能被打断),面板还挂着一个已不成立的错误。
|
|
111
|
+
- **成功的验令牌被报成失败**:凭据服务读状态一抛错就落进 catch,回报 `ok:false` 并往事件流写一条假 `error`。
|
|
112
|
+
- **「忘掉令牌」整条丢掉 `body.ok`**:凭据文件只读时删除失败,界面上却表现为「什么都没发生」——而该失败是刻意传播的(谎报成功比失败更糟)。
|
|
61
113
|
|
|
62
|
-
|
|
63
|
-
- `!llmAvailable` 分支漏清 `state.built`:残留 `built` 成为**下一次** publish 的回滚目标,注册失败时回滚会把一只 release 已调用过的适配器重新挂上 Host——回滚只在别处已出错时才跑,是最坏的发现时机。改为统一路由 `unregister`(与 `dsh-connect-agnes-token-plan` 对齐)。钉子:`provider-publish.test.mjs` §10(阴性对照实测:旧代码 `state.built` 仍是 `{adapter…}`)。
|
|
114
|
+
**修复发布状态机的两处自伤**
|
|
64
115
|
|
|
65
|
-
|
|
116
|
+
- 工厂加载的 rejection 被 memo 死:一次失败的 `import()` 把错误钉在槽里直到进程结束,此后 provider 再也注册不上,**照面板给的补救提示修好依然无效、必须重启 Host**。现在只 memo 成功,失败清槽、下一次 publish 真重试。
|
|
117
|
+
- `!llmAvailable` 分支漏清 `state.built`:残留 `built` 成为下一次 publish 的回滚目标,会把一只已 release 的适配器重新挂上 Host。
|
|
66
118
|
|
|
67
|
-
|
|
68
|
-
- **同类漂移的实证**:同一个坑在 `doctor.test.mjs` 里被手工 `delete` 处理过,`routes.test.mjs` 没有——「每个测试记得清环境」这条纪律在同一个仓库里已经出现了两种下场。修法因此不选"补一句 delete",而是给 `HostDeps` 加 `env` **注入缝**(与既有 `fetchImpl` / `getLlm` / `emit` 同列的既有标准测试缝):产品侧不传、落回 `process.env` 零漂移,测试侧传 `{}` 则宿主环境彻底进不了判定。
|
|
69
|
-
- **门禁**:新增 `test/env-hermetic.test.mjs`(已进 `npm test`),两层:①运行时对照——断言「默认 env 源确实能读到宿主令牌」(把泄漏路径的存在钉成事实,删掉它就没人知道第二条在防什么)与「注入 env 后看不见宿主令牌」「注入的 env 仍可用」(缝不是恒空的桩);②源码扫描——任何 import `apply()` 的测试必须隔离 env,忘了就在这里红并指明文件。**守纪律的是文件,不是记性。**
|
|
70
|
-
- 阴性对照:①临时造一个忘了隔离的测试文件 → 门禁列出它(`zz-negative-probe.test.mjs`);②删掉 `index.ts` 的 `env: deps.env` 转发 → §1 立刻红在「注入 env 缝后,宿主令牌不得可见」。两次都还原后全绿。
|
|
119
|
+
**「读不到」不再被画成 0**
|
|
71
120
|
|
|
72
|
-
|
|
121
|
+
- `usage.json` 不可读时,面板「额度」头条从「今日 0 次」变成「—」,并在面板上方给出原因。此前「读不到 ≠ 0」的语义在三个文件里写了三遍,一次都没兑现:真实的读失败被吞成空载荷再折成 0,而「读不到画成 0」会让操作者以为今天没调用过。
|
|
122
|
+
- 判据改为「文件在不在」:文件**不存在** = 真实的 0;**在却读不出来**(损坏 / 形状不对 / 未知版本 / 不可读)= `null`。
|
|
73
123
|
|
|
74
|
-
|
|
75
|
-
- **修法**:判据换成「**文件在不在**」。`usage.json` **不存在** = 真实的 0(这台 Host 从没调用过,那是已知答案);**文件在却读不出来**(损坏 / 形状不对 / 更新构建写的版本 / 存在却不可读)= `null`;存在与否由 `stat` 判定,且这发 IO 只落在异常路径上(正常循环被读缓存短路,零额外开销)。
|
|
76
|
-
- **用户可见**:`usage.json` 不可读时,面板「额度」头条从「今日 0 次」变成「—」,并在面板上方 `shapeWarnings` 里给出原因(此前至少还有一行警告,现在数字本身也不再伪造)。**把「读不到」画成 0 会让操作者以为今天没调用过——这正是那份注释说的错误判断。**
|
|
77
|
-
- **既有的旧期望随之更正**:`usage-store.test.mjs` 曾把「损坏 → 0」「形状坏 → 0」「未知版本 → 0」当成"不炸"的证据——它们证明的是没崩,不是答案是 0。现在三处都断言 `null`,并新增反向对照(文件不存在仍是真实的 0,且不带任何痕迹)。
|
|
78
|
-
- **端到端钉子**:`routes.test.mjs` 新增一节——用**真实挂载的插件** + 磁盘上一份 `version: 99` 的 `usage.json`(里面写着今日 7 次),断言快照头条是 `null` 而不是那 7 次、也不是 0,且 `usage-state:` 警告在场。
|
|
79
|
-
- 阴性对照:把 `daily()` 退回恒 0 后,`usage-store.test.mjs` 与 `routes.test.mjs` 的新断言双双红(actual 0 / expected null),还原后全绿。
|
|
80
|
-
- **留给 backlog**(本次有意不改,避免膨胀):`perModelToday()` 与 `trend()` 还没跟上同一套语义——它们仍把读不到渲染成空数组 / 全零桶。补齐要动 `wire.ts` 的 `TrendBucket` 形状与 client 渲染,属于契约变更,另起一轮。
|
|
124
|
+
**令牌状态补上真实结论**
|
|
81
125
|
|
|
82
|
-
|
|
126
|
+
- `valid` / `checkedAt` 此前在 Host 侧硬编码为 `null`,从无生产者,面板只能一直显示「未校验」。现在由零额度探针产出:200/400 → 有效,401/403 → 无效,**429 / 额度耗尽 / 上游 5xx 一律不写**(它们回答的是「上游忙不忙」,不是「令牌好不好」)。
|
|
127
|
+
- 换令牌时清掉旧结论;结论只活在本进程,重启回到「未校验」。
|
|
128
|
+
- 新增「无效」态:`valid: false` 此前会渲染成「未校验」——把已知的坏说成不知道。探针出结论后面板立即刷新,不再等下一个 30s 轮询。
|
|
83
129
|
|
|
84
|
-
|
|
85
|
-
- **修法**:三个接上(不传 = 落回 `ctx`,产品行为零漂移);`pollMs` **删除**(留着只会让人以为它能用——与当年移除 `dailyQuotaTotal` 同一条纪律)。反向也补:`loadToolsModule` / `drawFetch` 实际在用却从未声明(靠 `deps: any` 绕过类型),现已写进接口。
|
|
86
|
-
- **门禁**:新增 `test/deps-wiring.test.mjs`(已进 `npm test`)——①双向扫描:`HostDeps` 每个字段都必须在 `src/` 里被 `deps.<name>` 读到,反向(读了却没声明)同样红;②运行时:注入的 `getLlm` / `credentials` 必须真的生效。**两层各有各的阴性对照**:拆掉接线 → §1 红在「声明了没人读」;读到却不用(只当布尔判断)→ §2 红在「注入的 getLlm 必须真的被调用」——只有两层都在,才同时堵住「没接线」与「接了没用」。
|
|
87
|
-
- **顺带修掉 `docs.test.mjs` §10 的一处误报**:反重复规则的 needle 是通用词「静默忽略」,任何注释一用这四个字就被判成在重述 `reasoning_effort` 那条事实(实测误伤 `index.ts` 里一句关于 deps 缝的注释)。收窄为归属文件原文的指纹「无法区分『参数生效』与『参数被静默忽略』」——中途试过「无法区分」,它同样是通用词(误伤 `docs/SPIKE-image-gen.md`)。阴性对照:插入一句真重述,门禁仍能抓住。
|
|
130
|
+
**可用性与无障碍**
|
|
88
131
|
|
|
89
|
-
|
|
132
|
+
- tab 键盘焦点不可见:内联 `outline: "none"` 压掉了外壳的焦点环,改为 `outlineOffset: -2`。
|
|
133
|
+
- 令牌输入框补 `aria-label`、ToggleSwitch 补 `busyLabel`(屏幕阅读器此前读不出「正在切换」)。
|
|
134
|
+
- 模型 id 含 `#` / `?` 时详情页外链 URL 语义被破坏,改为分段 `encodeURIComponent`。
|
|
135
|
+
- 面板会显示「NaN 个模型」,补 `Number.isFinite`。
|
|
90
136
|
|
|
91
|
-
|
|
92
|
-
- **修法**:唯一的生产者是零额度探针(`noteValidity`)。判据按证据分,不按成败分:200/400 → `true`(鉴权过了);401/403 → `false`(令牌坏的证据);**429 / 额度耗尽 / 上游 5xx 一律不写**——它们回答的是"上游现在忙不忙",不是"这把令牌好不好",记成 false 就是把限频说成令牌坏(429 分诊那条修正的同一个误判换个地方)。这几种留着上一次的结论,那是已知的最好答案。
|
|
93
|
-
- **失效时机**:`save` / `forget` 换令牌时清掉旧结论——一个关于**别的值**的判断不许继续替新令牌说话(与 `ephemeral` 那条修正同一条纪律)。只活在本进程:重启即回到"未校验",那正是 `null` 的语义。
|
|
94
|
-
- **三态补齐**:`valid: false` 此前会渲染成「未校验」——把已知的坏说成不知道,与"读不到画成 0"同一类谎。新增 `validity.bad`(zh「无效」/ en "invalid"),`validity.no`(「未校验」)从此只服务 `null`。
|
|
95
|
-
- **面板**:探针结论写进快照后立刻 `load()` 一次,否则那行「状态:…(校验 …)」要等下一个 30s 轮询才从"未校验"变成答案,而按钮下方明明已经写着结论了。
|
|
96
|
-
- **钉子**:`credentials.test.mjs` §6(三态与换手失效)、`routes.test.mjs` 新增两节——①错误码映射(401 写 false;429/额度/5xx **一个都不写**,阴性对照:去掉错误码判断后 `actual [false] / expected []`);②**端到端**:真实挂载的插件,存令牌 → 桩 200 探针 → 快照 `token.valid === true`,桩 401 → `false`,再存一次别的令牌 → 回到 `null`。
|
|
97
|
-
- 阴性对照:把 `state()` 退回硬编码 null → 两处新断言双双红(actual null / expected true);还原后全绿。
|
|
137
|
+
**工程**
|
|
98
138
|
|
|
99
|
-
|
|
139
|
+
- 依赖安装不再被 npm 10 的自动 peer 解析卡死;构建工具链精确钉住版本(build-gate 逐字节比对产物)。
|
|
140
|
+
- CI 门禁补真 DOM 与真 React 两档渲染套件;测试环境隔离宿主令牌,测试缝接线双向扫描。细节与取证见下方指针。
|
|
100
141
|
|
|
101
|
-
|
|
102
|
-
|
|
142
|
+
> **实施过程(病灶、取证、判据、阴性对照)全部在
|
|
143
|
+
> [docs/archive/2026-10-16-0.3.0-process.md](docs/archive/2026-10-16-0.3.0-process.md)**——那几十轮里
|
|
144
|
+
> 不少是踩过的坑,照着做能绕开。
|
|
103
145
|
|
|
104
146
|
## [0.2.0] — 2026-10-05
|
|
105
147
|
|
package/README.md
CHANGED
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
三个 tab:**额度 / 模型 / 接入**。
|
|
12
12
|
|
|
13
|
-
**「额度」tab
|
|
13
|
+
**「额度」tab**——头条是官方魔粒余额(真实值),下面接本地调用分布与趋势图:
|
|
14
14
|
|
|
15
15
|

|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
趋势图是**竖向柱状图**(每根柱顶标出当天次数、0 次不标;轴约 7 个日期刻度且不带年份),区块内可切**显示天数**(默认 / 7 / 14 / 30 / 90)。这个选择器只决定「画多少天」,与决定「能回看多久」的**保留窗口**(`retentionDays`,默认 90 天)是两件事——显示窗口恒被夹在保留窗口之内,所以不会出现「把已剪掉的日子画成 0」那种与「那天真的没调用」无法区分的形状。选择保存在 `usage-prefs.json`(按 profile 分段)。
|
|
18
|
+
|
|
19
|
+
**「模型」tab**——模型按**厂商分组**(组头可折叠、带数量与已选数,「推荐」徽标挂在组头、推荐的三家置顶),组内一行一个魔搭模型、勾选即启用,顶部可搜索;开关打开即把魔搭注册为 DSH provider:
|
|
18
20
|
|
|
19
21
|

|
|
20
22
|
|
|
@@ -24,15 +26,24 @@
|
|
|
24
26
|
tools registry 没有 unregister)。实测协议(异步两段式、必需头、提交态不可信、
|
|
25
27
|
计费与耗时口径)见 [docs/SPIKE-image-gen.md](docs/SPIKE-image-gen.md)。
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
工具本身也是**两段式**的(v0.4 起):一次调用只等 `drawWaitMs`(默认 90 秒)——
|
|
30
|
+
图好了直接返回,没好就返回 `status: pending` 与一个 `jobId`,**带上它再调用即可
|
|
31
|
+
继续查询**(只查询、不重复出图)。上游排队实测 1–15 分钟,此前一次调用会一直
|
|
32
|
+
await 到出图为止,整轮对话都得陪着等。
|
|
33
|
+
|
|
34
|
+

|
|
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
|
+
说明——平台未亲口声明)。
|
|
36
47
|
|
|
37
48
|
## 对话框胶囊(v0.2 起)
|
|
38
49
|
|
|
@@ -81,7 +92,7 @@ tools registry 没有 unregister)。实测协议(异步两段式、必需头
|
|
|
81
92
|
|
|
82
93
|
## 诚实声明
|
|
83
94
|
|
|
84
|
-
- 面板头条是**官方魔粒余额**(真实值);本地计数只统计**经本插件的调用**——它回答的是分布/趋势/429
|
|
95
|
+
- 面板头条是**官方魔粒余额**(真实值);本地计数只统计**经本插件的调用**——它回答的是分布/趋势/429 事件流,不是余额。**它是「本客户端」的口径**:注册 provider 之后该客户端的全部魔搭调用都经本插件,但**不跨客户端**——桌面端与 Web 端是两个 profile,各有各的状态目录(`$DSH_HOME/state/<profile>/<插件名>/usage.json`),各记各的账、互不可见;直连魔搭的其它客户端同样不计入。`npm run doctor` 会把每个 profile 的用量并排列出来,这是「另一个客户端的数字去哪了」的答案。次数口径的「推算剩余」已从面板移除(官方改魔粒计费后两个单位并排是误导),相应地`quota.daily` 只剩 `usedLocal` 一个数——没有 `limit`,也没有推算出的「剩余」。这个数还有第三种答案:**本地计数文件读不到时它是 `null`,面板渲染「—」**——「今日 0 次」是可信的日常值,把「我不知道」画成它会让人以为今天没调用过(`usage.json` 损坏、形状不对、或被更新的构建写过都是这种情形,原因会在面板上方的漂移提示里说明)。
|
|
85
96
|
- **本地计数的口径与 harness 全局账本不同,且刻意不合并**。DSH 自己有 `@linxin666/dsh-usage`,按 `days→provider→model` 记成功调用的 token;它只订阅 session 的 `assistant/message`,**失败调用一次都不记**。所以本插件只做两件全局账本答不了的事:①「经本插件的调用分布与趋势」;②**429/错误事件流**——这是本插件的独家数据(官方只给总额度余额,不给「哪个模型刚才在限频」)。成功的 token 总量不在此重复记账(详见 `src/host/usage-observer.ts` 文件头)。
|
|
86
97
|
- 429/错误事件的分类读的是 **`llm-error-fix` 重分类之后**的 code:被误判成 QUOTA 的 rpm 限频在事件流里记作 `rate_limit`,与退避策略说同一件事。观察层套在重分类层**之外**,顺序不可反。
|
|
87
98
|
- **计数口径**:每次流结束记一次 call,**含失败、含用户中断**(魔搭按次数计费,限频掉的请求同样是一次调用);token 数上游没给时是 `null` 而非 0。偏差方向是**按流计次**:重试不在流内部发生,而是在流消费完后由 `agent/request-error` 瀑布决定重发,因而**每次重试各记一次**——按「魔搭按次数计费」的口径这反而是对的(每次重发都真打了一次上游)。
|
|
Binary file
|
|
Binary file
|
|
Binary file
|