@duke-dsh-plugins/dsh-token-stats 1.5.5 → 1.7.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.
Files changed (4) hide show
  1. package/README.md +5 -3
  2. package/client.js +84 -14
  3. package/index.js +376 -68
  4. package/package.json +11 -6
package/README.md CHANGED
@@ -61,11 +61,13 @@ dsh plugin --profile web add https://github.com/MoonlitDropOfBlood/dsh-token-sta
61
61
  4. **会话输入框工具行**(model 选择器左侧)内联显示当前 provider 的套餐余额:
62
62
  - DeepSeek / OpenRouter 显示余额(如 `¥43.97` / `$12.50`);
63
63
  - MiniMax / Kimi / 智谱 显示 `5h X% | 7d Y%`(5 小时 / 7 天窗口已用百分比,悬停查看重置时间);
64
- - 小米 MiMo 显示 `套餐 X% | 本月 Y%`(Token Plan 已用百分比,悬停查看重置时间;需在凭据里配 platform.xiaomimimo.com 的登录 Cookie,见下);
64
+ - 小米 MiMo 默认显示 `今日 X · 7d Y`(**本地用量统计,零凭据、永不过期**);若在「套餐余额」里填了 dashboard 登录 Cookie,则升级为官方 `剩余 X%` + 周重置倒计时(见下);
65
65
  - 切换模型时自动切换 provider;每 60s 自动刷新,**点击读数立即强制刷新**;
66
66
  - 未配置对应 API Key 或拉取失败时显示 `⚠`(悬停查看原因);当前 provider 不在支持列表时不占位。
67
67
 
68
- > **MiMo(小米)配置**:MiMo 的用量走 dashboard admin API(非公开 endpoint),纯 API Key(Bearer)实测会被 401 拒绝,可靠凭证是浏览器登录 Cookie。打开 platform.xiaomimimo.com → 订阅管理,F12 → Network 任一 `/api/v1/tokenPlan/*` 请求 → 复制完整 `Cookie` 请求头值,粘到 DSH 凭据的 `XIAOMI_MIMO_COOKIE`(或把 API Key 配到 `XIAOMI_MIMO_API_KEY`,插件会先 Bearer 后 Cookie 自动重试)。按 route 派生命名的凭据(设置页自动生成,如 `XIAOMI_TOKEN_PLAN_CN_API_KEY`)同样识别;composer 读数按 route id 匹配(`xiaomi-mimo` / `xiaomimimo` / `xiaomi-token-plan-cn` / 名字含 xiaomi 或 mimo 均可)。**v1.5.4 起也可以直接在「设置 → Token 统计 → 套餐余额 → 凭据设置」里粘贴保存**,无需手改凭据库(mimo 写入 `XIAOMI_MIMO_COOKIE`,不影响 LLM 路由的 API Key)。
68
+ > **MiMo(小米)配置**:**默认零配置**——读数来自本插件自己的会话日志聚合(今日 / 近 7 天消耗 token 数),不发任何 MiMo 请求、不会过期。缺点是拿不到套餐百分比(官方没开放任何 API Key 配额接口:`token-plan-*` 与 `api.` 主机只有 OpenAI 兼容的 `/v1` 推理 API,实测所有 Bearer 配额路径均 404;key 还与区域强绑定,CN key 在 SGP/AMS 主机一律 401)。
69
+ >
70
+ > 想要**官方剩余用量百分比**,唯一路径是 dashboard 账号会话:打开 platform.xiaomimimo.com 登录后,F12 → Network 任一 `/api/v1/*` 请求 → 复制完整 `Cookie` 请求头(含 `api-platform_serviceToken` 与 `userId`)→ 粘到「设置 → Token 统计 → 套餐余额 → **MiMo 官方剩余用量(可选)**」保存(写入 `XIAOMI_MIMO_COOKIE`)。插件内置最小 cookie jar:服务端若在响应里 `Set-Cookie` 续发会话会被自动吸收并写回凭据库(跨轮询、跨重启生效);**Cookie 彻底失效时自动静默退回本地用量,不会报错**。composer 读数按 route id 匹配(`xiaomi-mimo` / `xiaomimimo` / `xiaomi-token-plan-cn` / 名字含 xiaomi 或 mimo 均可)。
69
71
 
70
72
  ## 工作原理
71
73
 
@@ -88,7 +90,7 @@ TokenStatsService.getQuota() ← ctx.remote.tokenStats.getQuota(provider, forc
88
90
  - 数据按**本地日历天** × **模型**(`provider::model`)聚合;`total = input + output + cacheRead + cacheWrite`。
89
91
  - 历史回填只统计插件启动前发生的调用,实时监听只统计启动后的,两者通过每个会话的事件序号水位线合并,不会重复。
90
92
  - 模型/模型来自 `assistant/message` 的 `message.source`(kind = 'model'),无需自行解析请求头。
91
- - 套餐余额:Host 半经 `credentials` 服务解析用户在模型设置里已配置的 API Key(引用命名遵循 `<ROUTE>_API_KEY` 约定),GET provider 官方端点(宿主全局 fetch 优先,无 fetch 时回退 `subprocess` curl);30s 缓存 + 失败指数退避(5s→30min),不落盘。
93
+ - 套餐余额:Host 半经 `credentials` 服务解析用户在模型设置里已配置的 API Key(引用命名遵循 `<ROUTE>_API_KEY` 约定),GET provider 官方端点(宿主全局 fetch 优先,无 fetch 时回退 `subprocess` curl);30s 缓存 + 失败指数退避(5s→30min),不落盘。**小米 MiMo 是例外**:它没有 API Key 配额接口,默认用本地会话日志聚合出用量,填了 dashboard Cookie 才升级为官方剩余用量 %。
92
94
 
93
95
  ## 目录结构
94
96
 
package/client.js CHANGED
@@ -18,8 +18,16 @@
18
18
  * (`ctx.remote.tokenStats.getStats()/getQuota()`), published by the Host half
19
19
  * in `index.js`.
20
20
  */
21
+ /**
22
+ * This package's client-module id. It is both the `__ModuleLoader__.load` id and
23
+ * the HMR style owner (`data-plugin`) — client-modules keys style ownership off the
24
+ * registered id, so the two must stay the same string. See the `claimStyles` note
25
+ * where the stylesheet is created.
26
+ */
27
+ const CLIENT_BUNDLE_ID = "@duke-dsh-plugins/dsh-token-stats";
28
+
21
29
  window.__ModuleLoader__.load({
22
- id: "@duke-dsh-plugins/dsh-token-stats",
30
+ id: CLIENT_BUNDLE_ID,
23
31
  factory: (require) => {
24
32
  var module = { exports: {} };
25
33
  var exports = module.exports;
@@ -260,6 +268,13 @@ window.__ModuleLoader__.load({
260
268
  try {
261
269
  styleTag = document.createElement("style");
262
270
  styleTag.textContent = CSS;
271
+ // dsh 0.2.0 client-modules runs `claimStyles(ownerId)` during
272
+ // materialization, which claims every *untagged* <style> in the document
273
+ // for the materializing package. `apply()` is async and appends this tag
274
+ // after that sweep, so without an explicit owner it would stay untagged
275
+ // and a later package could adopt it — and `removeOwnedStyles(thatId)`
276
+ // would then strip our stylesheet on its HMR invalidation. Claim it now.
277
+ styleTag.setAttribute("data-plugin", CLIENT_BUNDLE_ID);
263
278
  document.head.appendChild(styleTag);
264
279
  ctx.effect(() => () => styleTag && styleTag.remove());
265
280
  } catch (e) {
@@ -667,9 +682,11 @@ window.__ModuleLoader__.load({
667
682
  if (lower.includes("mimo") || lower.includes("xiaomi")) return "mimo";
668
683
  return null;
669
684
  }
670
- // 凭据设置行写入的 ref — 与 host QUOTA_PROVIDERS.refs 首选项对齐。
671
- // mimo 写 XIAOMI_MIMO_COOKIE(README 文档路径),不触碰 LLM 路由的
672
- // API Key ref(host probe 顺序 Cookie 优先,v1.5.4)。
685
+ // 凭据设置行写入的 ref — 与 host QUOTA_PROVIDERS.apiKeyRefs 首选项对齐。
686
+ // mimo 写 XIAOMI_MIMO_COOKIE(v1.7.0 起):MiMo 没有开放任何 API Key
687
+ // 配额接口(token-plan-* 主机只有 /v1 推理 API,实测见 scripts/probe-mimo.mjs),
688
+ // 只有 dashboard 账号会话 Cookie 能拿到官方「剩余用量 %」。不填也不影响
689
+ // —— host 会退回本地用量统计。
673
690
  const QUOTA_CRED_REFS = {
674
691
  minimax: "MINIMAX_CN_API_KEY",
675
692
  deepseek: "DEEPSEEK_API_KEY",
@@ -708,8 +725,14 @@ window.__ModuleLoader__.load({
708
725
  return [labelEl, React.createElement("span", { key: "b", style: strong }, txt)];
709
726
  }
710
727
  if (provider === "mimo") {
711
- // MiMo Token Plan:套餐已用% + 本月总额度%(detail 缺失时可能只有一行)
728
+ // MiMo:官方读数显示「剩余 X%」(balanceText),本地用量显示
729
+ // 「今日 X · 7d Y」——行内位置窄,只取前半段,完整内容在悬停面板。
712
730
  const parts = [labelEl];
731
+ if (typeof d.balanceText === "string" && d.balanceText) {
732
+ const compact = d.balanceText.split(" · ")[0];
733
+ parts.push(React.createElement("span", { key: "bal", style: strong }, compact));
734
+ return parts;
735
+ }
713
736
  if (typeof d.fiveHrPct === "number") {
714
737
  parts.push(React.createElement("span", { key: "plan", style: strong }, "套餐 " + d.fiveHrPct + "%"));
715
738
  }
@@ -738,20 +761,24 @@ window.__ModuleLoader__.load({
738
761
  if (props.ok && props.display) {
739
762
  const d = props.display;
740
763
  if (typeof d.balanceText === "string" && d.balanceText) {
764
+ // mimo 的 balanceText 可能是「本地用量」也可能是官方「剩余 X%」。
765
+ const cap =
766
+ props.provider === "mimo"
767
+ ? (typeof d.weeklyPct === "number" ? "官方剩余用量" : "本地用量(近 7 天 / 今日)")
768
+ : "账户余额" + (d.currency ? " (" + d.currency + ")" : "");
741
769
  rows.push(
742
770
  React.createElement(
743
771
  "div", { key: "bal", className: "ts-quota-pop-row" },
744
- React.createElement("span", null, "账户余额" + (d.currency ? " (" + d.currency + ")" : "")),
772
+ React.createElement("span", null, cap),
745
773
  React.createElement("b", null, d.balanceText),
746
774
  ),
747
775
  );
748
776
  }
777
+ // mimo 官方读数只有一个 Token Plan 窗口(/api/v1/user/usage 的
778
+ // percent 是「剩余」,host 已换算成已用 %);本地用量模式没有窗口。
749
779
  const windows =
750
780
  props.provider === "mimo"
751
- ? [
752
- ["套餐已用", d.fiveHrPct, d.fiveHrResetsIn],
753
- ["本月总额度", d.weeklyPct, d.weeklyResetsIn],
754
- ]
781
+ ? [["Token Plan", d.weeklyPct, d.weeklyResetsIn]]
755
782
  : [
756
783
  ["5h 窗口", d.fiveHrPct, d.fiveHrResetsIn],
757
784
  ["7d 窗口", d.weeklyPct, d.weeklyResetsIn],
@@ -951,12 +978,17 @@ window.__ModuleLoader__.load({
951
978
  }
952
979
  const d = v.display || {};
953
980
  if (typeof d.balanceText === "string" && d.balanceText) {
981
+ // mimo 复用 balanceText 承载两种读数:本地用量(无窗口百分比)或
982
+ // 官方「剩余 X%」(有 weeklyPct),所以标题不能写死「账户余额」。
983
+ const head = p === "mimo"
984
+ ? (typeof d.weeklyPct === "number" ? "Token Plan" : "本地用量")
985
+ : "账户余额" + (d.currency ? " · " + d.currency : "");
954
986
  return React.createElement(
955
987
  "div", { className: "ts-card ts-quota-card" },
956
988
  React.createElement(
957
989
  "div", { className: "ts-quota-card-head" },
958
990
  React.createElement("span", { className: "ts-quota-card-name" }, label),
959
- React.createElement("span", { className: "ts-quota-card-status" }, "账户余额" + (d.currency ? " · " + d.currency : "")),
991
+ React.createElement("span", { className: "ts-quota-card-status" }, head),
960
992
  ),
961
993
  React.createElement("div", { className: "ts-quota-card-value" }, d.balanceText),
962
994
  );
@@ -1050,7 +1082,14 @@ window.__ModuleLoader__.load({
1050
1082
  const needsCred = names.filter(
1051
1083
  (p) => quotas[p] && quotas[p].ok === false && (quotas[p].kind === "unconfigured" || quotas[p].kind === "auth_failed"),
1052
1084
  );
1053
- if (configured.length === 0 && needsCred.length === 0) return null;
1085
+ // mimo 零凭据可用(本地用量),所以不会落进 needsCred;这里在它还处于
1086
+ // 本地模式时补一个**可选**的 Cookie 入口——粘了就能升级成官方剩余用量 %。
1087
+ const mimo = quotas.mimo;
1088
+ const mimoLocalMode =
1089
+ !!(mimo && mimo.ok === true) &&
1090
+ typeof mimo.display === "object" &&
1091
+ typeof mimo.display.weeklyPct !== "number";
1092
+ if (configured.length === 0 && needsCred.length === 0 && !mimoLocalMode) return null;
1054
1093
  return React.createElement(
1055
1094
  "div", { className: "ts-block" },
1056
1095
  React.createElement(
@@ -1081,7 +1120,6 @@ window.__ModuleLoader__.load({
1081
1120
  "div", { className: "ts-cred-block" },
1082
1121
  React.createElement("div", { className: "ts-block-title" }, "凭据设置(粘贴后保存,自动重新拉取)"),
1083
1122
  needsCred.map((p) => {
1084
- const isAuthFailed = quotas[p] && quotas[p].kind === "auth_failed";
1085
1123
  return React.createElement(
1086
1124
  "div", { key: p, className: "ts-cred-row" },
1087
1125
  React.createElement("span", { className: "ts-cred-label" }, quotaProviderLabel(p)),
@@ -1089,7 +1127,7 @@ window.__ModuleLoader__.load({
1089
1127
  type: "password",
1090
1128
  className: "ts-cred-input",
1091
1129
  placeholder: p === "mimo"
1092
- ? (isAuthFailed ? "凭据已失效:重新粘贴 Cookie 或 API Key" : "粘贴登录 Cookie 或 API Key")
1130
+ ? "粘贴 platform.xiaomimimo.com 的 Cookie 请求头(含 api-platform_serviceToken 与 userId)"
1093
1131
  : "粘贴 API Key",
1094
1132
  value: credDrafts[p] || "",
1095
1133
  onChange: (e) => setCredDrafts((d) => ({ ...d, [p]: e.target.value })),
@@ -1110,6 +1148,38 @@ window.__ModuleLoader__.load({
1110
1148
  }),
1111
1149
  )
1112
1150
  : null,
1151
+ // mimo 专用:可选的 dashboard Cookie —— 粘了把本地用量升级成官方
1152
+ // 「剩余用量 % + 周重置」;不粘也能正常显示本地用量统计。
1153
+ mimoLocalMode
1154
+ ? React.createElement(
1155
+ "div", { className: "ts-cred-block" },
1156
+ React.createElement("div", { className: "ts-block-title" }, "MiMo 官方剩余用量(可选)"),
1157
+ React.createElement(
1158
+ "div", { className: "ts-cred-row" },
1159
+ React.createElement("span", { className: "ts-cred-label" }, "Cookie"),
1160
+ React.createElement("input", {
1161
+ type: "password",
1162
+ className: "ts-cred-input",
1163
+ placeholder:
1164
+ "MiMo 未开放 API Key 配额接口;登录 platform.xiaomimimo.com 后从开发者工具复制完整 Cookie 请求头(含 api-platform_serviceToken 与 userId),可显示官方剩余 %(会过期,失效后自动退回本地用量)",
1165
+ value: credDrafts.mimo || "",
1166
+ onChange: (e) => setCredDrafts((d) => ({ ...d, mimo: e.target.value })),
1167
+ onKeyDown: (e) => {
1168
+ if (e.key === "Enter") saveCred("mimo", credDrafts.mimo || "");
1169
+ },
1170
+ }),
1171
+ React.createElement(
1172
+ "button", {
1173
+ className: "ts-cred-save",
1174
+ disabled: !!credSaving.mimo || !(credDrafts.mimo && credDrafts.mimo.trim()),
1175
+ onClick: () => saveCred("mimo", credDrafts.mimo || ""),
1176
+ },
1177
+ credSaving.mimo ? "保存中…" : "保存",
1178
+ ),
1179
+ credMsgs.mimo ? React.createElement("span", { className: "ts-cred-msg" }, credMsgs.mimo) : null,
1180
+ ),
1181
+ )
1182
+ : null,
1113
1183
  );
1114
1184
  }
1115
1185
 
package/index.js CHANGED
@@ -142,33 +142,61 @@ const QUOTA_PROVIDERS = {
142
142
  // 智谱特殊: Authorization 不加 "Bearer " 前缀 (来自 Musage zhipu.rs 注释)
143
143
  authStyle: "raw",
144
144
  },
145
- // Xiaomi MiMo Token Plan — dashboard admin API(非公开 endpoint):
146
- // Bearer 实测 401(session 守护),可靠路径是登录 Cookie;单个凭证值先
147
- // Bearer 后 Cookie 自动重试(对齐 Musage xiaomi.rs 的 BearerThenCookie)。
148
- // 在 DSH 凭据里把浏览器 DevTools 复制的完整 Cookie 或 API Key 配到
149
- // XIAOMI_MIMO_API_KEY / XIAOMI_MIMO_COOKIE(或 MIMO_*)任一 ref;route 派生
150
- // 名(设置页按 route 生成的 XIAOMI_TOKEN_PLAN_CN_API_KEY/_COOKIE,与
151
- // 其他 provider 的 MINIMAX_CN_* 同规则)同样识别。
152
- // Cookie 类 ref 排最前:README 的可靠路径是 Cookie,且 Token 统计页「凭据
153
- // 设置」写入 XIAOMI_MIMO_COOKIE——已存在但失效的 API Key(如 route 派生名
154
- // 下的 tp- key 被 dashboard 401 拒)不能挡住用户新粘贴 Cookie(v1.5.4)。
145
+ // Xiaomi MiMo Token Plan — **零凭据可用**(v1.7.0 起重写)。
146
+ //
147
+ // 实测(2026-09,scripts/probe-mimo.mjs / probe-mimo-nocookie.mjs):
148
+ // · token-plan-{cn,sgp,ams} 与 api. 主机**只有 OpenAI 兼容的 /v1 推理 API**,
149
+ // `/v1/user/balance`、`/api/v1/tokenPlan/usage`、`/api/v1/user/usage` 全部
150
+ // 404 —— MiMo 没有给 API Key 开任何配额接口。
151
+ // · key 与区域强绑定(CN key 在 sgp/ams/api 一律 401 invalid_key),所以也不
152
+ // 能"换主机重试"。
153
+ // · 唯一存在的配额接口是 platform dashboard 的 `/api/v1/user/usage`
154
+ // (`{percent, resetDate}`,percent = **剩余**百分比;也是 MiMo 桌面端
155
+ // IPC `mimo:getUserUsage` 的同一条链路),但它只认小米账号会话 Cookie。
156
+ // 拿 tp- key 冒充 serviceToken / X-Service-Token / ?apiKey= 全部 401。
157
+ // → v1.6.0 用户看到的 401 正是"把 API Key 打了只认 Cookie 的 dashboard",
158
+ // 与 key 是否有效无关。
159
+ //
160
+ // 因此不再使用 API Key 读配额(`apiKeyRefs` 已移除):默认走**本地用量**
161
+ // (本插件已有的会话日志聚合,零凭据、零过期),配置了 Cookie 时自动升级为
162
+ // 官方「剩余用量 % + 周重置」,Cookie 失效则静默退回本地读数。
155
163
  mimo: {
156
- refs: ["XIAOMI_MIMO_COOKIE", "XIAOMI_TOKEN_PLAN_CN_COOKIE", "XIAOMI_MIMO_API_KEY", "XIAOMI_TOKEN_PLAN_CN_API_KEY", "MIMO_COOKIE", "MIMO_API_KEY"],
157
- urls: {
158
- XIAOMI_TOKEN_PLAN_CN_API_KEY: "https://platform.xiaomimimo.com/api/v1/tokenPlan/usage",
159
- XIAOMI_TOKEN_PLAN_CN_COOKIE: "https://platform.xiaomimimo.com/api/v1/tokenPlan/usage",
160
- XIAOMI_MIMO_API_KEY: "https://platform.xiaomimimo.com/api/v1/tokenPlan/usage",
161
- XIAOMI_MIMO_COOKIE: "https://platform.xiaomimimo.com/api/v1/tokenPlan/usage",
162
- MIMO_API_KEY: "https://platform.xiaomimimo.com/api/v1/tokenPlan/usage",
163
- MIMO_COOKIE: "https://platform.xiaomimimo.com/api/v1/tokenPlan/usage",
164
- },
164
+ // dashboard 会话 Cookie(唯一能拿到官方套餐 % 的凭据)
165
+ cookieRefs: ["XIAOMI_MIMO_COOKIE", "XIAOMI_TOKEN_PLAN_CN_COOKIE", "MIMO_COOKIE"],
165
166
  parse: parseMimoResponse,
166
167
  },
167
168
  };
168
169
 
169
- /** Xiaomi MiMo dashboard admin API endpoints(usage + detail,鉴权同一凭证)。 */
170
- const MIMO_USAGE_URL = "https://platform.xiaomimimo.com/api/v1/tokenPlan/usage";
171
- const MIMO_DETAIL_URL = "https://platform.xiaomimimo.com/api/v1/tokenPlan/detail";
170
+ /** Xiaomi MiMo 常量 —— 唯一可用的配额接口在 dashboard,且只认账号会话 Cookie。
171
+ * 协议来源:MiMo 桌面端 IPC `mimo:getUserUsage`(逆向记录见
172
+ * https://github.com/ethanweave/mimo-composer-token-status/blob/main/docs/INVESTIGATION.md)、
173
+ * CodexBar docs/mimo.md(MIT)、quotas crate mimo.rs(Unlicense)。*/
174
+ const MIMO_PLATFORM = "https://platform.xiaomimimo.com";
175
+ /** 官方桌面端同款:GET {accountBase}/user/usage → { percent(剩余), resetDate }。*/
176
+ const MIMO_USER_USAGE_URL = `${MIMO_PLATFORM}/api/v1/user/usage`;
177
+ /** 旧版套餐明细端点(保留兜底:部分账号只有它给数;已用比例 + 重置时间)。*/
178
+ const MIMO_USAGE_URL = `${MIMO_PLATFORM}/api/v1/tokenPlan/usage`;
179
+ const MIMO_DETAIL_URL = `${MIMO_PLATFORM}/api/v1/tokenPlan/detail`;
180
+ /** dashboard 必需 Cookie 名(CodexBar docs/mimo.md):缺失就给明确指引。 */
181
+ const MIMO_COOKIE_REQUIRED = ["api-platform_serviceToken", "userId"];
182
+ /** dashboard admin API 需要的额外请求头(quotas crate mimo.rs 用 accept-language: en)。*/
183
+ const MIMO_PLATFORM_HEADERS = { "accept-language": "en" };
184
+
185
+ /** `"a=1; b=2"` → Map{name: value}(容忍空格、容忍值里有 `=`)。 */
186
+ function cookieStringToMap(s) {
187
+ const out = new Map();
188
+ for (const part of String(s || "").split(";")) {
189
+ const i = part.indexOf("=");
190
+ if (i <= 0) continue;
191
+ out.set(part.slice(0, i).trim(), part.slice(i + 1).trim());
192
+ }
193
+ return out;
194
+ }
195
+
196
+ /** Map → `"a=1; b=2"`。 */
197
+ function cookieMapToString(m) {
198
+ return [...m].map(([k, v]) => k + "=" + v).join("; ");
199
+ }
172
200
 
173
201
  function quotaBackoffMs(streak) {
174
202
  if (streak <= 0) return 0;
@@ -466,7 +494,7 @@ function parseMimoResponse(body, detailBody) {
466
494
  ok: false,
467
495
  provider: "mimo",
468
496
  kind: "auth_failed",
469
- message: "MiMo 凭据已失效(Cookie 过期或未登录),请到 platform.xiaomimimo.com 重新复制",
497
+ message: "MiMo 凭据已失效(API Key 或 Cookie 过期),请到 platform.xiaomimimo.com 续费或重新生成",
470
498
  };
471
499
  }
472
500
  return {
@@ -549,6 +577,86 @@ function parseMimoResponse(body, detailBody) {
549
577
  };
550
578
  }
551
579
 
580
+ // ----- xiaomi mimo /api/v1/user/usage parser (v1.7.0,官方桌面端同款链路) -----
581
+ // { "percent": 78, "resetDate": "2026-09-16" }
582
+ // **percent 是「剩余」百分比**(账户菜单「剩余用量 78%」),与上面
583
+ // tokenPlan/usage 的 items[].percent(**已用**比例)语义相反,别混用。
584
+ // wire 里 fiveHrPct/weeklyPct 一律是「已用 %」,所以取 100 - percent。
585
+
586
+ function parseMimoUserUsage(body) {
587
+ let json;
588
+ try {
589
+ json = typeof body === "string" ? JSON.parse(body) : body;
590
+ } catch {
591
+ return { ok: false, provider: "mimo", kind: "parse", message: "MiMo 响应 JSON 解析失败" };
592
+ }
593
+ if (!json || typeof json !== "object") {
594
+ return { ok: false, provider: "mimo", kind: "parse", message: "MiMo 响应不是对象" };
595
+ }
596
+ if (typeof json.code === "number" && json.code !== 0) {
597
+ if (json.code === 401 || (json.code >= 40100 && json.code < 40200)) {
598
+ return {
599
+ ok: false,
600
+ provider: "mimo",
601
+ kind: "auth_failed",
602
+ message: "MiMo Cookie 已失效,请重新登录 platform.xiaomimimo.com 并复制 Cookie 请求头",
603
+ };
604
+ }
605
+ return {
606
+ ok: false,
607
+ provider: "mimo",
608
+ kind: "server_error",
609
+ message: "MiMo 业务错误 code=" + json.code + " · " + (json.message || ""),
610
+ };
611
+ }
612
+ const data = json.data && typeof json.data === "object" ? json.data : json;
613
+
614
+ const raw = Number(data.percent);
615
+ if (!isFinite(raw)) {
616
+ return {
617
+ ok: false,
618
+ provider: "mimo",
619
+ kind: "parse",
620
+ message: "MiMo 响应缺少 percent 字段(keys: " + Object.keys(data).join(",") + ")",
621
+ };
622
+ }
623
+ const remaining = Math.max(0, Math.min(100, Math.round(raw)));
624
+ const used = 100 - remaining;
625
+
626
+ // resetDate 形如 "2026-09-16"(本地日),按当天 23:59:59 解释为到期时刻。
627
+ let resetsAt = null;
628
+ if (typeof data.resetDate === "string" && data.resetDate) {
629
+ const iso = data.resetDate.length <= 10 ? data.resetDate + "T23:59:59" : data.resetDate;
630
+ const t = Date.parse(iso);
631
+ if (!isNaN(t)) resetsAt = t;
632
+ }
633
+ const resetsIn = resetsAt ? formatResetsIn(resetsAt) : null;
634
+
635
+ return {
636
+ ok: true,
637
+ provider: "mimo",
638
+ display: {
639
+ // 剩余用量是单一窗口(账户菜单标「1 周」),映射到 7d 窗口位
640
+ weeklyPct: used,
641
+ weeklyResetsIn: resetsIn,
642
+ fiveHrPct: null,
643
+ fiveHrResetsIn: null,
644
+ balanceText: "剩余 " + remaining + "%",
645
+ balanceUsd: null,
646
+ currency: null,
647
+ },
648
+ };
649
+ }
650
+
651
+ /** 12345678 → "12.3M"(读数行/卡片用的紧凑 token 计数)。 */
652
+ function formatTokenCount(n) {
653
+ if (!isFinite(n) || n <= 0) return "0";
654
+ if (n >= 1e9) return (n / 1e9).toFixed(n >= 1e10 ? 0 : 1) + "B";
655
+ if (n >= 1e6) return (n / 1e6).toFixed(n >= 1e7 ? 0 : 1) + "M";
656
+ if (n >= 1e3) return (n / 1e3).toFixed(n >= 1e4 ? 0 : 1) + "K";
657
+ return String(Math.round(n));
658
+ }
659
+
552
660
  function formatResetsIn(resetsAtMs) {
553
661
  if (typeof resetsAtMs !== "number" || !resetsAtMs) return "";
554
662
  const ms = resetsAtMs - Date.now();
@@ -1035,30 +1143,63 @@ export class TokenStatsService extends TypertRemoteService {
1035
1143
  * each provider's shipped default `apiKeyEnv`; without the credentials
1036
1144
  * service (headless deployment) the process environment is consulted.
1037
1145
  */
1146
+ /**
1147
+ * Resolve credentials for one provider through the DSH credentials seam.
1148
+ * Ref candidates follow the settings page's `<ROUTE>_API_KEY` derivation and
1149
+ * each provider's shipped default `apiKeyEnv`; without the credentials
1150
+ * service (headless deployment) the process environment is consulted.
1151
+ *
1152
+ * v1.6.0 起,mimo 同时持有 API Key (Bearer) + Cookie (dashboard session) 两类
1153
+ * 凭据;其他 provider 只用 API Key。返回值总是
1154
+ * { apiKey, apiKeyRef, cookie, cookieRef },
1155
+ * 未配置那一路为 null。`_quotaActiveRef[provider + ":api" | ":cookie"]` 按
1156
+ * provider+kind 缓存上次命中的 ref(保持粘性,但不互锁)。
1157
+ */
1038
1158
  async _quotaApiKey(provider) {
1039
1159
  const cfg = QUOTA_PROVIDERS[provider];
1040
- if (!cfg) return { ref: null, key: null };
1160
+ if (!cfg) return { apiKey: null, apiKeyRef: null, cookie: null, cookieRef: null };
1041
1161
  const credentials = this.ctx.get("credentials");
1042
- const candidates = this._quotaActiveRef[provider]
1043
- ? [this._quotaActiveRef[provider], ...cfg.refs.filter((r) => r !== this._quotaActiveRef[provider])]
1044
- : cfg.refs;
1045
- for (const ref of candidates) {
1162
+ // 兼容新旧两种配置形态:新的是 {apiKeyRefs, cookieRefs},旧的是单数组 refs。
1163
+ const apiKeyRefList = cfg.apiKeyRefs || cfg.refs || [];
1164
+ const cookieRefList = cfg.cookieRefs || [];
1165
+
1166
+ const resolveOne = async (ref) => {
1046
1167
  try {
1047
1168
  if (credentials && typeof credentials.resolve === "function") {
1048
1169
  const hit = await credentials.resolve(ref);
1049
- if (hit && hit.value) {
1050
- this._quotaActiveRef[provider] = ref;
1051
- return { ref, key: hit.value };
1052
- }
1170
+ if (hit && hit.value) return hit.value;
1053
1171
  } else if (typeof process !== "undefined" && process.env && process.env[ref]) {
1054
- this._quotaActiveRef[provider] = ref;
1055
- return { ref, key: process.env[ref] };
1172
+ return process.env[ref];
1056
1173
  }
1057
1174
  } catch (e) {
1058
1175
  /* try the next ref */
1059
1176
  }
1060
- }
1061
- return { ref: null, key: null };
1177
+ return null;
1178
+ };
1179
+
1180
+ const resolveList = async (refList, activeKey) => {
1181
+ const active = this._quotaActiveRef[activeKey];
1182
+ const candidates = active
1183
+ ? [active, ...refList.filter((r) => r !== active)]
1184
+ : refList;
1185
+ for (const ref of candidates) {
1186
+ const v = await resolveOne(ref);
1187
+ if (v) {
1188
+ this._quotaActiveRef[activeKey] = ref;
1189
+ return { ref, value: v };
1190
+ }
1191
+ }
1192
+ return { ref: null, value: null };
1193
+ };
1194
+
1195
+ const api = await resolveList(apiKeyRefList, provider + ":api");
1196
+ const ck = await resolveList(cookieRefList, provider + ":cookie");
1197
+ return {
1198
+ apiKey: api.value,
1199
+ apiKeyRef: api.ref,
1200
+ cookie: ck.value,
1201
+ cookieRef: ck.ref,
1202
+ };
1062
1203
  }
1063
1204
 
1064
1205
  async _resolveCurl() {
@@ -1072,7 +1213,7 @@ export class TokenStatsService extends TypertRemoteService {
1072
1213
  }
1073
1214
 
1074
1215
  /** curl fallback for hosts without a global fetch (same shape as musage). */
1075
- async _curlGet(url, key, authStyle) {
1216
+ async _curlGet(url, key, authStyle, extraHeaders) {
1076
1217
  const subprocess = this.ctx.get("subprocess");
1077
1218
  if (!subprocess || typeof subprocess.spawn !== "function") {
1078
1219
  return { ok: false, kind: "network", message: "subprocess service 不可用, 且宿主无全局 fetch" };
@@ -1084,15 +1225,19 @@ export class TokenStatsService extends TypertRemoteService {
1084
1225
  : authStyle === "cookie"
1085
1226
  ? "Cookie: " + key
1086
1227
  : "Authorization: Bearer " + key;
1228
+ const argv = [
1229
+ c, "-sS",
1230
+ "--max-time", String(Math.floor(QUOTA_REQUEST_TIMEOUT_MS / 1000)),
1231
+ "-w", "\n%{http_code}",
1232
+ "-H", authHeader,
1233
+ "-H", "Accept: application/json",
1234
+ ];
1235
+ for (const [name, value] of Object.entries(extraHeaders || {})) {
1236
+ argv.push("-H", name + ": " + value);
1237
+ }
1238
+ argv.push(url);
1087
1239
  const handle = subprocess.spawn({
1088
- argv: [
1089
- c, "-sS",
1090
- "--max-time", String(Math.floor(QUOTA_REQUEST_TIMEOUT_MS / 1000)),
1091
- "-w", "\n%{http_code}",
1092
- "-H", authHeader,
1093
- "-H", "Accept: application/json",
1094
- url,
1095
- ],
1240
+ argv,
1096
1241
  cwd: "/",
1097
1242
  stdio: {
1098
1243
  stdin: "ignore",
@@ -1126,11 +1271,13 @@ export class TokenStatsService extends TypertRemoteService {
1126
1271
  return { ok: true, body };
1127
1272
  }
1128
1273
 
1129
- /** GET one provider endpoint with its API key; fetch first, curl fallback. */
1130
- async _httpGet(url, key, authStyle) {
1274
+ /** GET one provider endpoint with its API key; fetch first, curl fallback.
1275
+ * `extraHeaders` carries provider-specific headers (e.g. MiMo dashboard 的
1276
+ * accept-language: en),fetch 与 curl 两条路径都要带上。 */
1277
+ async _httpGet(url, key, authStyle, extraHeaders) {
1131
1278
  if (typeof fetch === "function") {
1132
1279
  try {
1133
- const headers = { accept: "application/json" };
1280
+ const headers = { accept: "application/json", ...(extraHeaders || {}) };
1134
1281
  // authStyle: undefined → Bearer; "raw" → 原样 Authorization(智谱);
1135
1282
  // "cookie" → 整串 Cookie 头(小米 MiMo dashboard admin API)。
1136
1283
  if (authStyle === "cookie") headers.cookie = key;
@@ -1141,20 +1288,25 @@ export class TokenStatsService extends TypertRemoteService {
1141
1288
  signal: AbortSignal.timeout(QUOTA_REQUEST_TIMEOUT_MS),
1142
1289
  });
1143
1290
  const body = await res.text();
1291
+ // 带回 Set-Cookie:mimo 的 cookie jar 靠它接住服务端续发的会话
1292
+ // (浏览器自动存,我们此前每次重发死 cookie,等于放弃了续期)。
1293
+ const setCookie = typeof res.headers.getSetCookie === "function" ? res.headers.getSetCookie() : [];
1144
1294
  if (res.status !== 200) {
1145
1295
  return {
1146
1296
  ok: false,
1147
1297
  kind: classifyQuotaHttpStatus(res.status),
1148
1298
  httpStatus: res.status,
1299
+ setCookie,
1149
1300
  message: "HTTP " + res.status + " · " + body.slice(0, 200),
1150
1301
  };
1151
1302
  }
1152
- return { ok: true, body };
1303
+ return { ok: true, body, setCookie };
1153
1304
  } catch (e) {
1154
1305
  return { ok: false, kind: "network", message: "fetch 失败: " + String((e && e.message) || e) };
1155
1306
  }
1156
1307
  }
1157
- return this._curlGet(url, key, authStyle);
1308
+ // curl 回退路径不带 Set-Cookie(无法维持会话续期,仅影响 mimo 的 cookie 升级)。
1309
+ return this._curlGet(url, key, authStyle, extraHeaders);
1158
1310
  }
1159
1311
 
1160
1312
  async _fetchProviderQuota(provider) {
@@ -1162,8 +1314,9 @@ export class TokenStatsService extends TypertRemoteService {
1162
1314
  if (!cfg) {
1163
1315
  return { ok: false, provider, kind: "other", message: "未知 provider: " + provider };
1164
1316
  }
1165
- const { ref, key } = await this._quotaApiKey(provider);
1166
- if (!key) {
1317
+ const { apiKey, apiKeyRef, cookie, cookieRef } = await this._quotaApiKey(provider);
1318
+ if (provider === "mimo") return this._fetchMimoQuota(apiKey, apiKeyRef, cookie, cookieRef);
1319
+ if (!apiKey) {
1167
1320
  return {
1168
1321
  ok: false,
1169
1322
  provider,
@@ -1171,29 +1324,184 @@ export class TokenStatsService extends TypertRemoteService {
1171
1324
  message: "未配置 " + provider + " API Key (在 DSH 模型设置里配置对应 provider)",
1172
1325
  };
1173
1326
  }
1174
- if (provider === "mimo") return this._fetchMimoQuota(key);
1175
- const url = cfg.urls[ref] || cfg.urls[cfg.refs[0]];
1176
- const raw = await this._httpGet(url, key, cfg.authStyle);
1327
+ const refList = cfg.apiKeyRefs || cfg.refs || [];
1328
+ const url = cfg.urls[apiKeyRef] || cfg.urls[refList[0]];
1329
+ const raw = await this._httpGet(url, apiKey, cfg.authStyle);
1177
1330
  if (!raw.ok) return { ...raw, provider };
1178
1331
  return cfg.parse(raw.body);
1179
1332
  }
1180
1333
 
1181
1334
  /**
1182
- * Xiaomi MiMo Token Plan(dashboard admin API,契约来自 Musage xiaomi.rs):
1183
- * 同一凭证值先按 Bearer 试,401/403 时原样改 `Cookie:` 头重试;usage 成功
1184
- * 后再 best-effort 拉一次 detail(周期结束时间 / 过期标记)。detail 失败
1185
- * 不影响 usage 结果(只是没有重置倒计时)。
1335
+ * MiMo 本地用量(零凭据、零过期):从会话日志聚合里筛出 MiMo 模型,
1336
+ * 算「今日 / 近 7 天(含今天)」消耗 token。provider 判定与 client 的
1337
+ * PROVIDER_ALIASES 同一套包含回退(route id 用户自由命名)。
1338
+ */
1339
+ _mimoLocalUsage() {
1340
+ const isMimo = (meta, key) => {
1341
+ const p = String((meta && meta.provider) || "").toLowerCase();
1342
+ if (p) return p.includes("mimo") || p.includes("xiaomi");
1343
+ const n = String((meta && meta.name) || key || "").toLowerCase();
1344
+ return n.includes("mimo");
1345
+ };
1346
+ const now = Date.now();
1347
+ const todayKey = this._dayKeyOf(now);
1348
+ const weekCut = this._dayKeyOf(now - 6 * DAY_MS);
1349
+ let today = 0;
1350
+ let week = 0;
1351
+ for (const [dk, day] of this._byDay) {
1352
+ if (dk > todayKey) continue;
1353
+ let dayTotal = 0;
1354
+ for (const [key, rec] of day) {
1355
+ const meta = this._modelMeta.get(key);
1356
+ if (!isMimo(meta, key)) continue;
1357
+ dayTotal += rec.total || 0;
1358
+ }
1359
+ if (dk === todayKey) today = dayTotal;
1360
+ if (dk >= weekCut) week += dayTotal;
1361
+ }
1362
+ return { today, week };
1363
+ }
1364
+
1365
+ /**
1366
+ * 最小 cookie jar:把响应 `Set-Cookie` 里属于 MiMo 的条目吸收进当前 cookie 串,
1367
+ * 并写回 DSH 凭据库,使服务端续发的会话能跨轮询、跨重启生效。
1368
+ *
1369
+ * 只吸收两类名字:jar 里已存在的(续期/轮换同一条 cookie),以及 MiMo 已知的那几个
1370
+ * (防止服务端塞进无关 cookie 污染用户粘贴的内容)。没有变化则不写盘。
1371
+ *
1372
+ * @returns {Promise<string|null>} 合并后的新 cookie 串;无变化返回 null。
1186
1373
  */
1187
- async _fetchMimoQuota(key) {
1188
- let raw = await this._httpGet(MIMO_USAGE_URL, key, undefined);
1189
- let cookie = false;
1190
- if (!raw.ok && (raw.httpStatus === 401 || raw.httpStatus === 403)) {
1191
- raw = await this._httpGet(MIMO_USAGE_URL, key, "cookie");
1192
- cookie = true;
1374
+ async _absorbMimoCookie(cookieRef, current, setCookieHeaders) {
1375
+ if (!Array.isArray(setCookieHeaders) || setCookieHeaders.length === 0) return null;
1376
+ const jar = cookieStringToMap(current);
1377
+ const known = new Set([...MIMO_COOKIE_REQUIRED, "api-platform_ph", "api-platform_slh"]);
1378
+ let changed = false;
1379
+ for (const raw of setCookieHeaders) {
1380
+ // 只取第一段 `name=value`,后面的 Path/HttpOnly/Expires 是属性不是 cookie
1381
+ const pair = String(raw).split(";")[0];
1382
+ const i = pair.indexOf("=");
1383
+ if (i <= 0) continue;
1384
+ const name = pair.slice(0, i).trim();
1385
+ if (!name) continue;
1386
+ if (!jar.has(name) && !known.has(name)) continue;
1387
+ const value = pair.slice(i + 1).trim();
1388
+ // 浏览器语义:空值或 Max-Age=0 表示**删除**该 cookie,不是把它置空
1389
+ // (置空会发出 `api-platform_serviceToken=`,等于带着坏 cookie 去请求)。
1390
+ if (value === "" || /;\s*max-age\s*=\s*0/i.test(raw)) {
1391
+ if (jar.delete(name)) changed = true;
1392
+ continue;
1393
+ }
1394
+ if (jar.get(name) === value) continue;
1395
+ jar.set(name, value);
1396
+ changed = true;
1193
1397
  }
1194
- if (!raw.ok) return { ...raw, provider: "mimo" };
1195
- const detail = await this._httpGet(MIMO_DETAIL_URL, key, cookie ? "cookie" : undefined);
1196
- return parseMimoResponse(raw.body, detail && detail.ok ? detail.body : null);
1398
+ if (!changed) return null;
1399
+ const next = cookieMapToString(jar);
1400
+ const credentials = this.ctx.get("credentials");
1401
+ // 服务端把会话 cookie 全清掉了 = 会话没了:删掉凭据比留个空串更贴近浏览器,
1402
+ // 下一轮会自然退回本地用量,而不是带着空 cookie 反复 401。
1403
+ if (!next) {
1404
+ try {
1405
+ if (cookieRef && credentials && typeof credentials.unset === "function") {
1406
+ await credentials.unset(cookieRef);
1407
+ }
1408
+ } catch {
1409
+ /* 写回失败不影响本次读数 */
1410
+ }
1411
+ return null;
1412
+ }
1413
+ // 写回官方凭据库(宿主侧 credentials.set);失败只影响跨重启续期,本进程内
1414
+ // 仍用 next 继续请求,所以吞掉异常。
1415
+ try {
1416
+ if (cookieRef && credentials && typeof credentials.set === "function") {
1417
+ await credentials.set(cookieRef, next);
1418
+ }
1419
+ } catch {
1420
+ /* 写回失败不影响本次读数 */
1421
+ }
1422
+ return next;
1423
+ }
1424
+
1425
+ /** MiMo 读数(v1.7.0):官方 Cookie 优先,失败静默退回本地用量。 */
1426
+ async _fetchMimoQuota(_apiKey, _apiKeyRef, cookie, cookieRef) {
1427
+ if (cookie) {
1428
+ const official = await this._fetchMimoOfficial(cookie, cookieRef);
1429
+ if (official.ok) return official;
1430
+ // Cookie 失效(auth_failed)→ 不打扰用户,静默退回本地用量。
1431
+ // 其余(套餐过期 / 解析失败 / 限流 / 5xx)都是真实状态,如实上报。
1432
+ if (official.kind !== "auth_failed") return official;
1433
+ }
1434
+ const local = this._mimoLocalUsage();
1435
+ return {
1436
+ ok: true,
1437
+ provider: "mimo",
1438
+ display: {
1439
+ fiveHrPct: null,
1440
+ weeklyPct: null,
1441
+ fiveHrResetsIn: null,
1442
+ weeklyResetsIn: null,
1443
+ balanceText: "今日 " + formatTokenCount(local.today) + " · 7d " + formatTokenCount(local.week),
1444
+ balanceUsd: null,
1445
+ currency: null,
1446
+ },
1447
+ };
1448
+ }
1449
+
1450
+ /**
1451
+ * 官方 dashboard 配额(仅 Cookie):主用桌面端同款 `/api/v1/user/usage`
1452
+ * (percent=剩余 + resetDate),失败再退到旧版 `/api/v1/tokenPlan/usage`
1453
+ * + `/detail`(已用比例)。两条都不通 = Cookie 失效。
1454
+ */
1455
+ async _fetchMimoOfficial(cookie, cookieRef) {
1456
+ const missing = MIMO_COOKIE_REQUIRED.filter((n) => cookie.indexOf(n) < 0);
1457
+ if (missing.length === MIMO_COOKIE_REQUIRED.length) {
1458
+ return {
1459
+ ok: false,
1460
+ provider: "mimo",
1461
+ kind: "auth_failed",
1462
+ message:
1463
+ "MiMo Cookie 缺少 " + MIMO_COOKIE_REQUIRED.join(" / ") +
1464
+ ":登录 platform.xiaomimimo.com 后从开发者工具复制完整 Cookie 请求头",
1465
+ };
1466
+ }
1467
+
1468
+ // 每个响应后立刻吸收 Set-Cookie:服务端续发的会话要马上用于后续请求
1469
+ // (browser jar 的等价行为),否则同一轮里还在用旧 token。
1470
+ const jar = async (res) => {
1471
+ const next = await this._absorbMimoCookie(cookieRef, cookie, res && res.setCookie);
1472
+ if (next) cookie = next;
1473
+ return res;
1474
+ };
1475
+
1476
+ const user = await jar(await this._httpGet(MIMO_USER_USAGE_URL, cookie, "cookie", MIMO_PLATFORM_HEADERS));
1477
+ if (user.ok) {
1478
+ const parsed = parseMimoUserUsage(user.body);
1479
+ if (parsed.ok) return parsed;
1480
+ // 业务码 401 = 会话已死,不用再试旧端点。
1481
+ if (parsed.kind === "auth_failed") return parsed;
1482
+ } else if (user.httpStatus === 401 || user.httpStatus === 403) {
1483
+ return { ...user, provider: "mimo" };
1484
+ } else if (user.kind === "rate_limited" || (user.httpStatus >= 500 && user.httpStatus < 600)) {
1485
+ return { ...user, provider: "mimo" };
1486
+ }
1487
+ // 404(该账号没有这个端点)或其它 → 退到旧版 tokenPlan/usage + detail。
1488
+ // detail 必须先拉(expired 标记 + 重置时间要在 usage 解析前拿到)。
1489
+ const detail = await jar(await this._httpGet(MIMO_DETAIL_URL, cookie, "cookie", MIMO_PLATFORM_HEADERS));
1490
+ const usage = await jar(await this._httpGet(MIMO_USAGE_URL, cookie, "cookie", MIMO_PLATFORM_HEADERS));
1491
+ if (usage.ok) {
1492
+ const parsed = parseMimoResponse(usage.body, detail.ok ? detail.body : null);
1493
+ // plan_expired / schema_unknown / parse 都是"拿到了但内容不对",如实上报;
1494
+ // 只有 auth_failed 才继续降级。
1495
+ if (parsed.ok || parsed.kind !== "auth_failed") return parsed;
1496
+ }
1497
+ return {
1498
+ ok: false,
1499
+ provider: "mimo",
1500
+ kind: "auth_failed",
1501
+ message:
1502
+ "MiMo Cookie 已失效(" + MIMO_USER_USAGE_URL.replace("https://", "") +
1503
+ " 返回 " + (user.httpStatus || "异常") + "):重新登录 platform.xiaomimimo.com 后复制 Cookie",
1504
+ };
1197
1505
  }
1198
1506
 
1199
1507
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@duke-dsh-plugins/dsh-token-stats",
3
- "version": "1.5.5",
4
- "description": "Token usage statistics for DeepSeek Harness (7/30-day per-model charts + yearly heatmap) plus an inline coding-plan balance/quota readout in the composer (minimax/deepseek/kimi/openrouter/zhipu).",
3
+ "version": "1.7.0",
4
+ "description": "Token usage statistics for DeepSeek Harness (7/30-day per-model charts + yearly heatmap) plus an inline coding-plan balance/quota readout in the composer (minimax/deepseek/kimi/openrouter/zhipu/mimo; mimo falls back to local usage since MiMo exposes no API-key quota API).",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/MoonlitDropOfBlood/dsh-token-stats.git"
@@ -27,7 +27,7 @@
27
27
  ],
28
28
  "license": "MIT",
29
29
  "engines": {
30
- "dsh": ">=0.1.4-rc.2 <0.2.0"
30
+ "dsh": ">=0.1.4-rc.2 <0.3.0"
31
31
  },
32
32
  "dsh": {
33
33
  "bundle": {
@@ -39,8 +39,8 @@
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@deepseek-ai/cordis": "^4.0.1",
42
- "@deepseek-ai/dsh": ">=0.1.4-rc.2 <0.2.0",
43
- "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.7"
42
+ "@deepseek-ai/dsh": ">=0.1.4-rc.2 <0.3.0",
43
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.7 || ^0.2.0-rc.1"
44
44
  },
45
45
  "dependencies": {
46
46
  "zod": "^4.4.3"
@@ -50,6 +50,11 @@
50
50
  "smoke": "node scripts/smoke-store.mjs",
51
51
  "smoke:quota": "node scripts/smoke-quota.mjs",
52
52
  "smoke:robust": "node scripts/smoke-robust-host.mjs && node scripts/smoke-robust-client.mjs",
53
- "smoke:typert": "node scripts/smoke-typert.mjs"
53
+ "smoke:typert": "node scripts/smoke-typert.mjs",
54
+ "smoke:compat": "node scripts/smoke-compat.mjs",
55
+ "probe:mimo": "node scripts/probe-mimo.mjs",
56
+ "probe:mimo:nocookie": "node scripts/probe-mimo-nocookie.mjs",
57
+ "probe:mimo:session": "node scripts/probe-mimo-session.mjs",
58
+ "e2e": "npm run check && npm run smoke && npm run smoke:quota && npm run smoke:robust && npm run smoke:typert && npm run smoke:compat && node scripts/repro-client-mount.mjs && node scripts/repro-strict-dispatch.mjs && node scripts/repro-profile-boot.mjs"
54
59
  }
55
60
  }