dsh-connect-modelscope-token-plan 0.2.0 → 0.3.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,5 +1,106 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0] — 2026-10-16
4
+
5
+ **新增「上下文窗口」全局档位 + 内置策展表(三级真值链)**(魔搭目录 `/v1/models` 不返回任何窗口字段——每个模型此前一律按 128K 兜底声明,Host 的计费/截断行为全部建立在这个默认值上)
6
+
7
+ - **三级真值链**:目录真声明了窗口就用目录值(不受任何下层影响)→ 用户在面板「上下文窗口」选择器选档(默认/128K/256K/512K/1M,落盘 `provider.json` 的 `contextWindow` 键,v1 形状新增可选键,旧构建宽容读、不 bump 版本)→ 用户没设覆盖时查内置策展表 `src/host/context-catalog.ts`(models.dev 各厂 host 同 id 交叉比对取多数,2026-10-16 核对,当前目录 19 个 id 有数据:DeepSeek-V4 全系 1M、Qwen3.5 系/Step 3.x 256K、GLM-5.2 1M 等)→ 表里也没有才落 128K 兜底。零网络依赖:策展表编译期内置,不运行时拉 api.json。
8
+ - **打标在唯一入口**:`fetchModels` 拉回目录后在 `normalizeEntry` 前宽形状打标(目录声明过窗口的条目原样保留,只对未声明的删原始四键、写覆盖值),descriptor/roster/`catalogSignature` 全链路自动吃到;`catalogSignature` 含窗口值,保存即自动重注册。缓存键带覆盖指纹(`models:<override>`),避免 TTL 内读到未打标条目。
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` 钉策展表形状与命中语义。
12
+
13
+ **新增对话框用量胶囊**(输入框工具栏 `conversation.input.right` 座席,只在会话当前选中的 provider 是 `modelscope-token-plan` 时出现)
14
+
15
+ - 正文两个读数、各标各的来源:`魔搭 · 143 魔粒 · 今日本地 7 次`。前半是**官方魔粒余额**(`snapshot.balance.available`),后半是 Host 侧本地日桶(`snapshot.quota.daily.usedLocal`)——后者的「本地」二字是承重的,官方接口没有「今日」这一项,不写清就会被读成官方计数(口径见 README「诚实声明」)。
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「诚实声明」一节,指针已改。
22
+
23
+ **CI 门禁修好两处「上游数据引发的红」**(都与功能改动无关:同一个提交在 10-06 是绿的,10-15 重跑同一提交也红)
24
+
25
+ - **npm 10(Node 22 自带)装不上依赖**:`npm error Cannot read properties of null (reading 'edgesOut')`,十几秒就死在 `npm install`。根因链条实测为:tsdown 声明可选 peer `@vitejs/devtools: "*"` → 该包 `0.8.0`(2026-10-14 发布)的 peer 里新增 `@vitejs/devtools-vitest` → vitest 的可选 peer 是 jsdom / happy-dom / canvas,**npm 10 的 peer 解析在那里崩**,npm 11 正常。`overrides` 回退 rolldown / cac / rolldown-plugin-dts 都无效、`--omit=optional` 无效,而 `--legacy-peer-deps` 立即成功——所以崩点在 peer 解析而非某个包。修法:项目根加 `.npmrc`(`legacy-peer-deps=true`),CI 与本地开发者(npm 10)一起修好,不必改 workflow。本仓库的 peerDependencies **全部**标了 optional,peer 校验本就没有可执行对象,关掉自动 peer 解析无实际损失。
26
+ - **build-gate 报「client.js 与当前 src 不同源」**:lockfile 有意不入库,于是 CI 全新安装解析到 **tsdown 0.23.1 + rolldown 1.2.14**,打出的字节与入库产物不同——门禁红在一个「看着像改了 src 忘 build」的地方,真因是工具链漂了。修法:把工具链精确钉在**产出当前入库产物**的那一组(`tsdown: 0.23.0` + `overrides: rolldown 1.2.12 / rolldown-plugin-dts 0.28.6`),并在 `tsdown.config.mjs` 头部写下升级流程(改 pin 必须把新产物与 pin 放进同一个提交)。验证方式:在没有 lockfile 的干净 worktree 上全新安装,钉住后 build-gate 逐字节绿、不钉则与 CI 同样红。
27
+
28
+ **门禁:新增真 DOM 渲染/交互套件 `test/dom-render.test.mjs`**(devDependencies 加 `jsdom`,是本仓库第一个测试用第三方包;仍然离线,不联网)
29
+
30
+ - 补的缺口:client 半边此前只由 `test/panel.test.mjs` 的**替身 React**钉着——`createElement` 返回一个普通对象,`onClick` 只是对象上一个属性值,于是事件绑定、受控输入、busy 期间该不该 `disabled`、effect 与卸载的时序全都不可观察;更糟的是替身把 children 放在 `props` **旁边**,而 `SectionCard` 从 `props` 里解构 `children`——折叠体里的卡片在那个套件里**从来没被渲染过**。新套件把**构建产物 `client.js`** 挂进 jsdom 的真 document,配一套最小替身 React(hook 槽位按「路径 + 组件名」、setState 触发真重渲染、effect 在 commit 之后跑、卸载跑 cleanup),钉住:九条面板路由各自被真实点击驱动过且 payload 正确(打字经 state 回流进 `/token` 的 body、哨兵独占态、`已启用 N / M` 与勾选态一致)、`disabled` 的控件点不动、挂载即首轮轮询与 locale 订阅、切后台 `clearInterval` / 回前台补一发 / 失败退避不低于 60s、顶替与卸载真的 abort 在途请求、慢响应不覆盖快响应、写失败与契约漂移那两句话真的上屏。
31
+ - 阴性对照:同一批 16 条 src 变异(拆 handler、把开关值取反、混排哨兵、不 `clearInterval`、不记 `inFlight`、`ok:false` 判成功、缺键不报漂移……),`panel.test.mjs` 抓住 **0** 条,新套件抓住 **16** 条。跑法与「为什么必须带 `DSH_DOM_TARGET=src`」(产物入口看不见 src 变异,首轮曾得到 24/24 全绿的假象)见 `tools/dev/README.md`;spec 是 `tools/dev/mutate-dom-render.json`。
32
+ - 这一档没覆盖的部分见下一条(真 React 那一档),以及 `docs/ROADMAP.md` 的 Backlog:并发与错误边界、fake timers 下跑满 cadence 的长周期行为。
33
+
34
+ **再加一档:真 React 渲染门禁 `test/dom-render-react.test.mjs`**(devDependencies 精确钉 `react@18.3.1` + `react-dom@18.3.1`)
35
+
36
+ - 上面那档 mini 替身每次 commit 整棵重建,所以「React 的 reconciler 说了算」的那一类它**结构上没法表达**。这一档用真 `react-dom/client` + jsdom 挂同一块面板(**不引** jest/vitest/@testing-library:React 18.3 自带 `act`,`HTMLInputElement` 的原生 setter 够用),只承载五类:真 CSSOM 里选中态与未选中态确有区别、重渲染复用**同一批 DOM 节点**(对象身份)、重渲染**不丢焦点**、roster 换序后勾选跟着模型 id 走、React 开发期告警必须为零。焦点那条不是学术问题:面板每 30s 一次 `setData` 就重渲染一次,正在输入令牌的框被打断是用户会遇到的事,而 mini 档实测那里 `activeElement = BODY`、节点复用三条全 false——它根本看不见。
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 的值追踪器判成「没有变化」。写第一版时当场踩了前者。
40
+
41
+ **写守卫与注册状态机的六处修正**(每条都做了阴性对照:把修复前的源码放回去,新用例确实红)
42
+
43
+ - **允许清单三处判据漂移**:`snapshot-aggregate.ts` 与 `routes/provider.ts` 把**可读的空清单**当「不过滤」(正确),而 `index.ts` 的轮询写成 `stored.length > 0` 才算数、否则回退内存——于是操作者按 `versionNote` 给的建议删掉 `provider.json`(或清单被保存为空)之后,面板说 `allowed:"all"` 且 `enabledCount` 数上全部模型,轮询却继续按内存里的旧清单注册:面板与 Host 对「正在提供什么」各说一套。判据收进 `provider-store.resolveEnabledIds` 一份,三处共用(只有读**抛错**才回退内存)。钉子:`provider.test.mjs` §9/§11(阴性对照:旧源码下 §11 的源码级断言直接红)。
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` 红)。
49
+
50
+ **代码质量收敛**(内部重构 + 门禁强化,用户可见的只有 a11y 与两处防御性修正)
51
+
52
+ - tab 键盘焦点不可见:`outline: "none"` 是内联样式,优先级高于外壳的焦点环,把键盘用户的焦点提示也一起压掉了。改为 `outlineOffset: -2`,保留可见性。
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 条。
59
+
60
+ **发布状态机的两处自伤**(受控复制的分叉实证:兄弟仓各自持有对方缺的修复)
61
+
62
+ - 工厂加载的 rejection 被 memo 死:`createAdapterFactoryResolver` memo 的对象是 promise 本身,一次失败的 `import()`(典型 `ERR_MODULE_NOT_FOUND`)把错误钉在槽里直到进程结束——此后每次 publish 重抛同一个错,provider 再也注册不上。要命的是 `describeBuildFailure` 的补救提示恰好叫人修安装,照提示修好依然无效、必须重启 Host。现在只 memo 成功,失败清槽、下一次 publish 真重试(与 `dsh-connect-sensenova-token-plan` 对齐)。钉子:`provider-publish.test.mjs` §9(阴性对照实测:旧代码第二次 publish `ok:false`)。
63
+ - `!llmAvailable` 分支漏清 `state.built`:残留 `built` 成为**下一次** publish 的回滚目标,注册失败时回滚会把一只 release 已调用过的适配器重新挂上 Host——回滚只在别处已出错时才跑,是最坏的发现时机。改为统一路由 `unregister`(与 `dsh-connect-agnes-token-plan` 对齐)。钉子:`provider-publish.test.mjs` §10(阴性对照实测:旧代码 `state.built` 仍是 `{adapter…}`)。
64
+
65
+ **测试环境隔离:宿主令牌不得进测试判定**(2026-10-16 实测,这台开发机上 `npm test` 是红的)
66
+
67
+ - **现场**:`test/routes.test.mjs:146` 断言「空状态快照的 `token.present === false`」,实测拿到 `true`——`createTokenStore` 的 env 默认 `process.env`,测试进程**继承了宿主的真实魔搭令牌**(本机 `MODELSCOPE_API_KEY` 在场,长 39)。清空该变量后全绿。**方向是最坏的**:CI(干净环境)永远绿,偏偏只有装了真令牌的机器红,也就是作者和每个真实用户。
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 缝后,宿主令牌不得可见」。两次都还原后全绿。
71
+
72
+ **本地计数的 `null` 终于真的产出**(语义修复,**用户可见的行为变更**)
73
+
74
+ - **病灶**:`wire.ts` 的 `DailyUsage.usedLocal: number | null` 里,null 那条分支("没读到")在 Host 侧**不可达**——`snapshot-aggregate` 只在 `usageStore.daily()` **抛异常**时才给 null,而真实的 `daily()` 把所有读失败都吞成空载荷再折成 0。`routes.test.mjs` 早有一条「读不到 → null,绝不是 0」的断言,但它只能用一个会抛错的**桩**造出那条路径,注释里写着「挂载的真实 store 造不出『读的时候抛』」。于是"读不到≠0"这件事在 `wire.ts` / `cards.ts` / `snapshot-aggregate.ts` 写了三遍,一次都没兑现。
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 渲染,属于契约变更,另起一轮。
81
+
82
+ **测试缝的接线审计:声明了 ≠ 接上了**(2026-10-16,同类漂移的第三例)
83
+
84
+ - **现场**:`HostDeps` 声明 5 个缝,`src/host/index.ts` 却只读了 `fetchImpl`(转给 inference-client):`credentials` / `getLlm` / `emit` 三个硬编码走 `ctx`,**注入被静默忽略**;`pollMs` 描述的机制根本不存在(它自己的注释就写着"轮询实际取 settings.pollSeconds")。纸面缝比没有缝更坏——测试以为自己覆盖了假 llm 服务,实际走的是 ctx,绿得毫无意义。而 ROADMAP 的头条 backlog(peer 装配链零覆盖)恰恰得靠这些缝去补。
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`)。阴性对照:插入一句真重述,门禁仍能抓住。
88
+
89
+ **令牌状态的两个字段终于真的产出**(`valid` / `checkedAt`,同类漂移的第四例,**用户可见**)
90
+
91
+ - **病灶**:`TokenStatus.valid: boolean | null` 与 `checkedAt: string | null` 在 Host 侧**硬编码为 null**——`ms-auth.state()` 直接写死,注释还写着"`valid` 恒为 null:本模块不做有效性断言"。于是 `valid === true` 与 `checkedAt !== null` 是**死分支**:wire 声明了三态、client 早已渲染 `validity.yes`、i18n 键齐备,唯独上游从不喂它。和 `usedLocal` 那条同形——下游全建好,中间一段没接。
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);还原后全绿。
98
+
99
+ **两处 P3 收敛**(无行为变更)
100
+
101
+ - `routes.ts` 对 `registerTokenRoute` 的调用结果做 `as Array<() => void>` 断言:token 是本族唯一注册两条路由(save + forget)的模块,返回类型标注在**产地**(函数签名)而不是在消费端断言——断言放在消费端等于把「这两条返回的是注销回调而不是 handler」的约定藏起来。去掉断言后 typecheck 仍绿,证明它是纯噪音。
102
+ - `shared/wire.ts` 的 `TokenStatus` 文档注释与 `export` 挤在同一行。
103
+
3
104
  ## [0.2.0] — 2026-10-05
4
105
 
5
106
  **凭据与红线**
package/README.md CHANGED
@@ -18,6 +18,35 @@
18
18
 
19
19
  ![「模型」tab:接入为DSH 模型与模型清单](assets/tab-model-list.png)
20
20
 
21
+ 同一 tab 下方另有**「出图工具」**卡(v0.2 起):开启后 Host 给 agent 注册
22
+ `modelscope_draw_image`,支持文生图、图生图与多图编辑。**它与「接入为 DSH 模型」
23
+ 是两条独立开关**,且开关值立即保存、而工具的**挂载发生在下次 Host 启动**(peer 的
24
+ tools registry 没有 unregister)。实测协议(异步两段式、必需头、提交态不可信、
25
+ 计费与耗时口径)见 [docs/SPIKE-image-gen.md](docs/SPIKE-image-gen.md)。
26
+
27
+ 同一 tab 下方另有**「上下文窗口」**选择器(默认/128K/256K/512K/1M):魔搭目录
28
+ `/v1/models` 不返回窗口字段,Host 侧按三级真值链给每个模型定窗口——目录真声明了
29
+ 就用目录值(不受任何下层影响);用户在这里选一个档位即对**目录没声明窗口的模型**
30
+ 全局生效,保存即重注册;**用户没设覆盖时**,插件查内置策展表(`context-catalog.ts`,
31
+ 来自 models.dev 各厂 host 交叉比对的同 id 主流窗口,2026-10-16 核对,含 DeepSeek-V4
32
+ 全系 1M、Qwen3.5/Qwen3.8-27B/Step 3.x 256K、GLM-5.2 1M 等 19 个当前目录 id);
33
+ 表里也没有的模型才落 128K 兜底。档位落盘在 `provider.json` 的 `contextWindow` 键
34
+ (v1 形状的新增可选键,旧构建宽容读),roster 每行会标出该模型当前生效的窗口——
35
+ 策展表来的值带 `*` 号与悬停说明(平台未亲口声明),与目录声明/用户覆盖区分。
36
+
37
+ ## 对话框胶囊(v0.2 起)
38
+
39
+ 注册 provider 之后,输入框工具栏会多一枚胶囊(用户不需要打开 Plugins 页就能看见额度):
40
+
41
+ ```
42
+ 魔搭 · 143 魔粒 · 今日本地 7 次
43
+ ```
44
+
45
+ - **前半**是官方魔粒余额(`balance.available`,真实值,与面板头条同一个数);**后半**是今日本地调用次数(Host 侧本地日桶,只统计经本插件的调用)。两个数字来源不同,所以后段必须带「本地」——官方接口没有「今日」这一项,不写清就会被读成官方计数。
46
+ - **只在会话当前选中的 provider 是 `modelscope-token-plan` 时出现**(读宿主 session 作用域的 `modelSelection` 投影),切到别的 provider 即整块消失,且一个请求都不发。
47
+ - **没有百分比、没有黄色档**。魔粒余额是**存量**、官方没有发布任何上限或低额阈值:画进度条或定一个「低于 N 魔粒算黄」的阈值,都是给一个没人公布过的分母配数。三档只有绿(读数是正数)/ 红(官方读数确实是 0)/ 灰(读不到)。
48
+ - **读不到 ≠ 0**(与面板同一条纪律):读不到时该段渲染「—」,0 只在官方真的给了 0 时出现并走告警色;读失败保留上一次的数值,并标「上次数据」(阈值 = Host 声明的 `pollSeconds` × 3)。tooltip 里带余额时间、失败原文与本地计数缺失的原因。
49
+
21
50
  ## 三条事实(写代码前先认清)
22
51
 
23
52
  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))。推理响应本身仍不带任何额度头。
@@ -41,8 +70,10 @@
41
70
  | 何时 | 查 |
42
71
  |---|---|
43
72
  | 「上游到底有没有额度信号」 | [docs/SPIKE.md](docs/SPIKE.md)(实测记录 + 结论) |
73
+ | 「能不能接生图、协议是什么」 | [docs/SPIKE-image-gen.md](docs/SPIKE-image-gen.md)(2026-10-10 实测;**v0.2 起已实现**,承上表体例) |
44
74
  | provider 怎么接入(写代码前) | [docs/PROVIDER-M4.md](docs/PROVIDER-M4.md)(实现契约) |
45
- | **这版是怎么做出来的、踩过什么坑** | [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md)(实施过程档案) |
75
+ | **这版是怎么做出来的、踩过什么坑** | [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md)(0.1.0 / M4 实施过程档案) |
76
+ | v0.2.0 的十二轮修复细节(已冻结,非入门路径) | [docs/archive/2026-10-05-closeout.md](docs/archive/2026-10-05-closeout.md) |
46
77
  | 上游端点全景 / 第三方方案对比 | [docs/REFERENCES.md](docs/REFERENCES.md)、[docs/REFERENCE-modelsdev.md](docs/REFERENCE-modelsdev.md) |
47
78
  | v1 要做什么、做到哪了 | [docs/ROADMAP.md](docs/ROADMAP.md) |
48
79
  | Host/Client 两半怎么分 | [docs/PROVIDER-M4.md](docs/PROVIDER-M4.md)(§14 红线)+ [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md);本仓库暂无独立的 `ARCHITECTURE.md` |
@@ -50,7 +81,7 @@
50
81
 
51
82
  ## 诚实声明
52
83
 
53
- - 面板头条是**官方魔粒余额**(真实值);本地计数只统计**经本插件的调用**——它回答的是分布/趋势/429 事件流,不是余额,直连魔搭的其它客户端不计入。注册 provider 之后,DSH 的全部魔搭调用都经本插件,所以本地计数**覆盖全部 DSH 魔搭调用**。次数口径的「推算剩余」已从面板移除(官方改魔粒计费后两单位并排是误导),相应地`quota.daily` 只剩 `usedLocal` 一个数——没有 `limit`,也没有推算出的「剩余」。
84
+ - 面板头条是**官方魔粒余额**(真实值);本地计数只统计**经本插件的调用**——它回答的是分布/趋势/429 事件流,不是余额,直连魔搭的其它客户端不计入。注册 provider 之后,DSH 的全部魔搭调用都经本插件,所以本地计数**覆盖全部 DSH 魔搭调用**。次数口径的「推算剩余」已从面板移除(官方改魔粒计费后两单位并排是误导),相应地`quota.daily` 只剩 `usedLocal` 一个数——没有 `limit`,也没有推算出的「剩余」。这个数还有第三种答案:**本地计数文件读不到时它是 `null`,面板渲染「—」**——「今日 0 次」是可信的日常值,把「我不知道」画成它会让人以为今天没调用过(`usage.json` 损坏、形状不对、或被更新的构建写过都是这种情形,原因会在面板上方的漂移提示里说明)。
54
85
  - **本地计数的口径与 harness 全局账本不同,且刻意不合并**。DSH 自己有 `@linxin666/dsh-usage`,按 `days→provider→model` 记成功调用的 token;它只订阅 session 的 `assistant/message`,**失败调用一次都不记**。所以本插件只做两件全局账本答不了的事:①「经本插件的调用分布与趋势」;②**429/错误事件流**——这是本插件的独家数据(官方只给总额度余额,不给「哪个模型刚才在限频」)。成功的 token 总量不在此重复记账(详见 `src/host/usage-observer.ts` 文件头)。
55
86
  - 429/错误事件的分类读的是 **`llm-error-fix` 重分类之后**的 code:被误判成 QUOTA 的 rpm 限频在事件流里记作 `rate_limit`,与退避策略说同一件事。观察层套在重分类层**之外**,顺序不可反。
56
87
  - **计数口径**:每次流结束记一次 call,**含失败、含用户中断**(魔搭按次数计费,限频掉的请求同样是一次调用);token 数上游没给时是 `null` 而非 0。偏差方向是**按流计次**:重试不在流内部发生,而是在流消费完后由 `agent/request-error` 瀑布决定重发,因而**每次重试各记一次**——按「魔搭按次数计费」的口径这反而是对的(每次重发都真打了一次上游)。